Overview

Check out the Function Syntax page for more information on how to create and validate Stratus functions and the Overview page before you start..

These templates are starting points. You should modify them based on your specific requirements and add appropriate error handling.

Here are some common function templates you can use as starting points for your Stratus functions. Simply copy the code and replace the placeholder values with your specific parameters.

This function rewards users for bridging assets through relay.link. Replace the constants in the first lines of the function with your actual values.

There are two core steps to this function both of which can exist as part of any function:

  1. It tracks all incoming transfers to the DESTINATION_CHAIN_ID.
  2. It uses the Coingecko API for price tracking. It requires a minimum of 1$ worth of coins to be bridged over and the dollar amount is multiplied by the REWARD_MULTIPLIER variable.

Function Code

const axios = require('axios')

/**
 * Required consts for the function
 */
const LOYALTY_API_KEY = 'REPLACE_WITH_YOUR_API_KEY'
const LOYALTY_RULE_ID = 'REPLACE_WITH_YOUR_LOYALTY_RULE_ID'
const REWARD_MULTIPLIER = '1' // USD value will be multiplied by 1
const DESTINATION_CHAIN_ID = 'REPLACE_WITH_CHAINID'
const GAS_TOKEN_ID = 'eth' // replace with the gas token ID of the destination chain

/**
 * Additional logger functionality
 */
const logs = []
const errors = []
const log = Object.assign(
  (message) => {
    logs.push(message)
  },
  {
    error: (errorMessage) => {
      errors.push(errorMessage)
    },
    logs,
    errors,
  }
)

/**
 * Get reward amount
 */
function rewardAmount(amount) {
  const number = parseFloat(amount)
  if (isNaN(number) || number < 1) return 0
  return Math.floor(number * REWARD_MULTIPLIER)
}

/**
 * Mark loyalty rule as completed to reward user
 */
async function completeLoyaltyRule(reward, log) {
  try {
    if (!LOYALTY_API_KEY) {
      throw new Error('LOYALTY_API_KEY is not defined')
    }

    await axios.post(
      `https://admin.snagsolutions.io/api/loyalty/rules/${LOYALTY_RULE_ID}/complete`,
      {
        verifyOnly: 'false',
        amount: reward.reward,
        walletAddress: reward.walletAddress,
        idempotencyKey: reward.txHash,
      },
      {
        headers: {
          'Content-Type': 'application/json',
          'X-API-KEY': LOYALTY_API_KEY,
        },
      }
    )

    log(
      `${reward.walletAddress} rewarded with ${reward.reward} points for $${reward.amount} transaction, rule ${LOYALTY_RULE_ID}`
    )
  } catch (error) {
    log.error(`Failed for ${reward.walletAddress}: ${error.message}`)
  }
}

/**
 * Get recent relay requests
 */
async function getRelayRequests() {
  const limit = 20
  const endTimestamp = Math.floor(Date.now() / 1000)
  const startTimestamp = endTimestamp - 10 * 60

  const allResults = []
  let continuation = null

  try {
    if (!DESTINATION_CHAIN_ID) {
      throw new Error('DESTINATION_CHAIN_ID is not defined')
    }

    do {
      let baseUrl = 'https://api.relay.link/requests/v2'
      const queryParams = [
        `sortBy=createdAt`,
        `destinationChainId=${DESTINATION_CHAIN_ID}`,
        `startTimestamp=${startTimestamp}`,
        `endTimestamp=${endTimestamp}`,
        `limit=${limit}`,
      ]

      if (continuation) {
        queryParams.push(`continuation=${encodeURIComponent(continuation)}`)
      }

      const url = `${baseUrl}?${queryParams.join('&')}`

      const response = await axios.get(url)
      const data = response.data
      if (Array.isArray(data.requests)) {
        allResults.push(...data.requests)
      }
      continuation = data.continuation
    } while (continuation)

    return allResults
  } catch (error) {
    log.error(`[getRelayRequests] Error fetching relay requests`)
    log.error(error)
    return []
  }
}

/**
 * Get recent relay rewards
 */
async function getRecentRewards() {
  const txArray = await getRelayRequests()
  const rewards = []

  try {
    if (!txArray || txArray.length === 0) {
      log('No new rewards found')
      return rewards
    }

    for (const tx of txArray) {
      if (tx?.status === 'success' && tx?.user) {
        const currencyInChainId =
          tx?.data?.metadata?.currencyIn?.currency?.chainId

        if (currencyInChainId === DESTINATION_CHAIN_ID) {
          continue
        }
        const amountUsd =
          tx?.data?.metadata?.currencyOut?.amountUsdCurrent ||
          tx?.data?.metadata?.currencyIn?.amountUsdCurrent ||
          0
        if (!amountUsd) continue

        const currency = tx?.data?.currency
        if (!currency || !['usdc', GAS_TOKEN_ID].includes(currency)) continue

        const txHash = tx?.data?.outTxs?.[0]?.hash

        const amountToReward = rewardAmount(amountUsd)

        if (amountToReward > 0) {
          rewards.push({
            walletAddress: tx.recipient,
            amount: amountUsd,
            reward: amountToReward,
            txHash: txHash,
          })
        }
      }
    }
  } catch (e) {
    log.error(`[getRecentRewards] Error fetching relay requests`)
    log.error(e)
  }
  return rewards
}

/**
 * Reward users
 */
async function rewardUsers(rewards, log) {
  for (const reward of rewards) {
    await completeLoyaltyRule(reward, log)
  }
}

module.exports.handler = async (input, output) => {
  try {
    const rewards = await getRecentRewards()
    if (rewards.length === 0) {
      log('No rewards found')
    } else {
      log(`Found ${rewards.length} rewards`)
      await rewardUsers(rewards, log)
    }

    // Set result
    output.setResult({
      logs,
      rewards,
      errors,
    })
    return output.buildOutput()
  } catch (error) {
    log.error('Error in user code:', error)
    throw error
  }
}

Transaction Entries On-Chain

Some projects prefer to host points onchain to generate transaction volume and decentralize points data. This function listens to transaction entries and mints/burns tokens based on account balance changes. It can be adjusted to any token contract.

  1. Click Add New Function
  2. In the Subscription field, either select an existing Snag Subscription, or create a new one, as in the image below.

  1. Finally, replace the LOYALTY_CURRENCY_ID with your loyalty currency ID and the ERC20_CONTRACT_ADDRESS with your token contract address in the first lines.
  2. Click Save and you’re good to go.

Function Code

const { encodeFunctionData } = require('viem')

// Replace with your contract address
const ERC20_CONTRACT_ADDRESS = '0x0000000000000000000000000000000000000000'
// Replace with your loyalty currency ID
const LOYALTY_CURRENCY_ID = 'YOUR_LOYALTY_CURRENCY_ID'

module.exports.handler = async (input, output) => {
  let operations = []

  // Parse input if it's a string
  if (typeof input === 'string') {
    try {
      let parsed = JSON.parse(input)
      if (typeof parsed === 'string') {
        parsed = JSON.parse(parsed)
      }
      input = parsed
    } catch (err) {
      output.setResult({
        message: 'Invalid JSON input',
        error: err,
        input: input,
        operations,
      })
      return output.buildOutput()
    }
  }

  if (!Array.isArray(input)) {
    output.setResult({
      message: 'Expected input to be an array of events',
      input: input,
      operations,
    })
    return output.buildOutput()
  }

  const mintABI = [
    {
      inputs: [
        { internalType: 'address', name: 'to', type: 'address' },
        { internalType: 'uint256', name: 'amount', type: 'uint256' },
      ],
      name: 'mint',
      outputs: [],
      stateMutability: 'nonpayable',
      type: 'function',
    },
  ]

  const burnABI = [
    {
      inputs: [
        { internalType: 'address', name: 'from', type: 'address' },
        { internalType: 'uint256', name: 'amount', type: 'uint256' },
      ],
      name: 'burn',
      outputs: [],
      stateMutability: 'nonpayable',
      type: 'function',
    },
  ]

  try {
    for (const event of input) {
      if (!event.data) continue

      const dataItem = event.data
      const tokenAmount = BigInt(dataItem.amount) * BigInt(10) ** BigInt(18)

      // Replace with your loyalty currency ID
      if (dataItem.loyaltyCurrencyId !== LOYALTY_CURRENCY_ID) {
        continue
      }

      let operationType = null
      if (dataItem.direction === 'credit') {
        operationType = 'mint'
      } else if (dataItem.direction === 'debit') {
        operationType = 'burn'
      } else {
        continue
      }

      const walletAddress = dataItem.loyaltyAccount?.user?.walletAddress
      if (!walletAddress) continue

      let data
      if (operationType === 'mint') {
        data = encodeFunctionData({
          abi: mintABI,
          functionName: 'mint',
          args: [walletAddress, tokenAmount.toString()],
        })
      } else if (operationType === 'burn') {
        data = encodeFunctionData({
          abi: burnABI,
          functionName: 'burn',
          args: [walletAddress, tokenAmount.toString()],
        })
      }

      output.addTransaction({
        to: ERC20_CONTRACT_ADDRESS,
        data: data,
        value: '0x0',
      })
    }

    output.setResult({
      message: 'Token transactions created.',
      operations,
    })

    return output.buildOutput()
  } catch (error) {
    console.error('Error in user code:', error)
    throw error
  }
}

DEX Trading Rewards

This function tracks and rewards users based on their trading volume, using Redis for persistence. It is based on a subscription to a USDC based uniswap pool, tracking the Swap event.

Replace the constants in the first lines of the function with your actual values.

Function Code

const { Redis } = require('ioredis')
const axios = require('axios')

const LOYALTY_RULE_ID = 'YOUR_LOYALTY_RULE_ID'
const LOYALTY_API_KEY = 'YOUR_API_KEY'
const REDIS_URL = 'YOUR_REDIS_URL'
const VOLUME_THRESHOLD = 10 // $10 USD threshold

const redis = new Redis(REDIS_URL)

/**
 * Mark loyalty rule as completed to reward user
 */
async function completeLoyaltyRule(walletAddress) {
  if (!LOYALTY_API_KEY) {
    throw new Error('LOYALTY_API_KEY is not defined')
  }

  await axios.post(
    `https://admin.snagsolutions.io/api/loyalty/rules/${LOYALTY_RULE_ID}/complete`,
    {
      verifyOnly: 'false',
      walletAddress: walletAddress,
    },
    {
      headers: {
        'Content-Type': 'application/json',
        'X-API-KEY': LOYALTY_API_KEY,
      },
    }
  )
}

module.exports.handler = async (input, output) => {
  const operations = []
  try {
    operations.push('Starting handler execution: parsing input')
    if (typeof input === 'string') {
      let parsed = JSON.parse(input)
      if (typeof parsed === 'string') parsed = JSON.parse(parsed)
      input = parsed
    }

    if (!Array.isArray(input) || input.length === 0) {
      output.setResult({ message: 'No event data provided', input, operations })
      return output.buildOutput()
    }

    const event = input[0]
    if (!event.decodedEvent || !event.decodedEvent.args) {
      throw new Error('Event data missing decodedEvent.args')
    }
    const args = event.decodedEvent.args

    // Process swap events (assuming token0 is USDC with 6 decimals)
    if (args.hasOwnProperty('amount0')) {
      const { sender, amount0 } = args
      const traderWallet = sender
      const tradeDollarValue = Math.abs(Number(amount0)) / 1e6

      const redisVolumeKey = `${traderWallet}-swapVolume`
      const redisRewardedKey = `${traderWallet}-swapRewarded`

      const alreadyRewarded = await redis.get(redisRewardedKey)
      if (alreadyRewarded) {
        output.setResult({
          message: `User ${traderWallet} has already been rewarded for swap volume.`,
          operations,
        })
        return output.buildOutput()
      }

      const storedVolumeStr = await redis.get(redisVolumeKey)
      const storedVolume = storedVolumeStr ? parseFloat(storedVolumeStr) : 0
      const newVolume = storedVolume + tradeDollarValue

      await redis.set(redisVolumeKey, newVolume.toString())

      if (storedVolume < redisRewardedKey && newVolume >= VOLUME_THRESHOLD) {
        // Complete loyalty rule when threshold is reached
        await completeLoyaltyRule(traderWallet)
        await redis.set(redisRewardedKey, 'true')

        output.setResult({
          message: `User ${traderWallet} rewarded for reaching $${VOLUME_THRESHOLD} swap volume.`,
          cumulativeSwapVolume: newVolume,
          operations,
        })
        return output.buildOutput()
      }

      output.setResult({
        message: `User ${traderWallet} cumulative swap volume updated to $${newVolume.toFixed(2)}.`,
        cumulativeSwapVolume: newVolume,
        operations,
      })
      return output.buildOutput()
    }

    output.setResult({ message: 'Unknown event type', operations })
    return output.buildOutput()
  } catch (error) {
    operations.push(`Error encountered: ${error.message}`)
    console.error('Error in handler:', error)
    output.setError(error.message)
    output.setResult({ operations })
    return output.buildOutput()
  }
}

Usage Notes

  1. Replace placeholder values (marked with YOUR_*) with your specific parameters
  2. Update Redis URLs with your instance details
  3. Adjust reward multipliers and thresholds as needed
  4. Test thoroughly in development before deploying to production

Remember to handle your API keys and sensitive data securely. Never commit them directly in your code.

Common Parameters to Replace

  • LOYALTY_RULE_ID: Your specific loyalty rule identifier
  • REDIS_URL: Your Redis instance connection string
  • ERC20_CONTRACT_ADDRESS: The address of your token contract
  • VOLUME_THRESHOLD: Minimum volume required for rewards
  • MULTIPLIER: Reward calculation multiplier

These templates are starting points. You should modify them based on your specific requirements and add appropriate error handling.