Set up a sponsor service#

The sponsor service runs on a secure backend and holds the sponsor wallet’s private key. It creates transactions and signs them on behalf of the sponsor.

Note

A full working example of a dApp using sponsored transactions is available on GitHub.

Code walkthrough#

Each section of the code above is broken down below to explain what is happening at each stage.

gRPC client and wallet setup

The ConcordiumGRPCNodeClient connects to a Concordium node, which is needed to query on-chain data such as account nonces.

The sponsor wallet is loaded from an exported wallet.export file. parseWallet reads the wallet export format, and buildAccountSigner creates a signer that can produce cryptographic signatures on behalf of the sponsor account.

Building token transfer operations

The ops array describes what the transaction does. Each operation uses TokenOperationType.Transfer to move tokens to a recipient. TokenAmount.fromDecimal converts a human-readable amount (e.g. "10.5") into the on-chain representation using the token’s decimal places. An optional CborMemo can be attached to annotate the transfer.

Creating the transaction builder and nonce

Transaction.tokenUpdate returns a transaction builder targeting a specific token by its ID. The builder pattern allows you to chain metadata and sponsorship before finalizing the transaction.

The sender’s next nonce is fetched from the chain via getNextAccountNonce. The nonce increments with each transaction and prevents replay attacks.

Building the sponsorable transaction

The builder’s addMetadata method sets the sender’s address, nonce, and expiry. addSponsor(sponsorAccount) designates the sponsor as the fee payer instead of the sender. The build() call produces a transaction ready for sponsorship.

Signing and serialization

Transaction.sponsor adds the sponsor’s cryptographic signature, committing them to pay the fees. The result is converted to JSON via Transaction.toJSON.

JavaScript’s native JSON.stringify cannot serialize BigInt values, so a custom replacer converts small values to numbers and large values to strings to avoid precision loss.

Serve as an API endpoint#

To expose sponsorTokenTransfer as a POST endpoint, create a file called server.ts:

import express from 'express'
import cors from 'cors'
import { sponsorTokenTransfer } from './sponsor'

const app = express()
app.use(cors())
app.use(express.json())

app.post('/sponsor', async (req, res) => {
    try {
        const { sender, recipient, amount, tokenId, decimals } = req.body

        if (!sender || !recipient || !amount || !tokenId || decimals === undefined) {
            return res.status(400).json({ error: 'Missing required fields' })
        }

        const sponsoredTransaction = await sponsorTokenTransfer(
            sender,
            recipient,
            amount,
            tokenId,
            Number(decimals)
        )

        return res.json({ sponsoredTransaction })
    } catch (error) {
        console.error('Sponsor error:', error)
        return res.status(500).json({ error: 'Failed to sponsor transaction' })
    }
})

const PORT = process.env.PORT || 3000
app.listen(PORT, () => {
    console.log(`Sponsor service running on port ${PORT}`)
})

Install Express, CORS, and their types:

$ npm install express cors
$ npm install -D @types/express @types/cors

Run the server:

$ npx ts-node server.ts

The frontend (covered in Create a sponsored transaction) calls POST /sponsor with the transfer details and receives back the sponsor-signed transaction for the user’s wallet to co-sign and submit.

Was this article helpful?