Skip to main content
Snippets of code that can be used to implement flows inside MiniPay
Make sure you are using Typescript v5 or above and Viem v2 or above.

Get the connected user’s address without any Library

// The code must run in a browser environment and not in node environment
if (window && window.ethereum) {
  // User has a injected wallet

  if (window.ethereum.isMiniPay) {
    // User is using Minipay

    // Requesting account addresses
    let accounts = await window.ethereum.request({
      method: "eth_requestAccounts",
      params: [],
    });

    // Injected wallets inject all available addresses,
    // to comply with API Minipay injects one address but in the form of array
    console.log(accounts[0]);
  }

  // User is not using MiniPay
}

// User does not have a injected wallet
To use the code snippets below, install the following packages:
yarn add @celo/abis @celo/identity viem@2

Check cUSD Balance of an address

import { getContract, formatEther, createPublicClient, http } from "viem";
import { celo } from "viem/chains";
import { stableTokenABI } from "@celo/abis";

// cUSD address on Celo mainnet
const STABLE_TOKEN_ADDRESS = "0x765DE816845861e75A25fCA122bb6898B8B1282a";

async function checkCUSDBalance(publicClient, address) {
  const StableTokenContract = getContract({
    abi: stableTokenABI,
    address: STABLE_TOKEN_ADDRESS,
    client: publicClient,
  });

  const balanceInBigNumber = await StableTokenContract.read.balanceOf([
    address,
  ]);

  const balanceInWei = balanceInBigNumber.toString();
  const balanceInEthers = formatEther(balanceInWei);

  return balanceInEthers;
}

const publicClient = createPublicClient({
  chain: celo,
  transport: http(),
}); // Mainnet

const balance = await checkCUSDBalance(publicClient, address); // In Ether unit

Check If a transaction succeeded

import { createPublicClient, http } from "viem";
import { celo } from "viem/chains";

async function checkIfTransactionSucceeded(publicClient, transactionHash) {
  const receipt = await publicClient.getTransactionReceipt({
    hash: transactionHash,
  });

  return receipt.status === "success";
}

const publicClient = createPublicClient({
  chain: celo,
  transport: http(),
}); // Mainnet

const transactionStatus = await checkIfTransactionSucceeded(
  publicClient,
  transactionHash
);

Estimate Gas for a transaction (in Celo)

import { createPublicClient, http } from "viem";
import { celo } from "viem/chains";

async function estimateGas(publicClient, transaction, feeCurrency = "") {
  return await publicClient.estimateGas({
    ...transaction,
    feeCurrency: feeCurrency ? feeCurrency : "",
  });
}

const publicClient = createPublicClient({
  chain: celo,
  transport: http(),
});

const gasLimit = await estimateGas(publicClient, {
  account: "0x8eb02597d85abc268bc4769e06a0d4cc603ab05f",
  to: "0x4f93fa058b03953c851efaa2e4fc5c34afdfab84",
  value: "0x1",
  data: "0x",
});

Estimate Gas for a transaction (in cUSD)

import { createPublicClient, http } from "viem";
import { celo } from "viem/chains";

async function estimateGas(publicClient, transaction, feeCurrency = "") {
  return await publicClient.estimateGas({
    ...transaction,
    feeCurrency: feeCurrency ? feeCurrency : "",
  });
}

const publicClient = createPublicClient({
  chain: celo,
  transport: http(),
});

const STABLE_TOKEN_ADDRESS = "0x765DE816845861e75A25fCA122bb6898B8B1282a";

const gasLimit = await estimateGas(
  publicClient,
  {
    account: "0x8eb02597d85abc268bc4769e06a0d4cc603ab05f",
    to: "0x4f93fa058b03953c851efaa2e4fc5c34afdfab84",
    value: "0x1",
    data: "0x",
  },
  STABLE_TOKEN_ADDRESS
);

Estimate Gas Price for a transaction (in Celo)

import { createPublicClient, http } from "viem";
import { celo } from "viem/chains";

async function estimateGasPrice(publicClient, feeCurrency = "") {
  return await publicClient.request({
    method: "eth_gasPrice",
    params: feeCurrency ? [feeCurrency] : [],
  });
}

const publicClient = createPublicClient({
  chain: celo,
  transport: http(),
});

const gasPrice = await estimateGasPrice(publicClient);

Estimate Gas Price for a transaction (in cUSD)

import { createPublicClient, http } from "viem";
import { celo } from "viem/chains";

async function estimateGasPrice(publicClient, feeCurrency = "") {
  return await publicClient.request({
    method: "eth_gasPrice",
    params: feeCurrency ? [feeCurrency] : [],
  });
}

const publicClient = createPublicClient({
  chain: celo,
  transport: http(),
});

const STABLE_TOKEN_ADDRESS = "0x765DE816845861e75A25fCA122bb6898B8B1282a";

const gasPrice = await estimateGasPrice(publicClient, STABLE_TOKEN_ADDRESS);

Calculate cUSD to be spent for transaction fees

import { createPublicClient, http, formatEther, fromHex } from "viem";
import { celo } from "viem/chains";

const publicClient = createPublicClient({
  chain: celo,
  transport: http(),
});

const STABLE_TOKEN_ADDRESS = "0x765DE816845861e75A25fCA122bb6898B8B1282a";

// `estimateGas` implemented above
const gasLimit = await estimateGas(
  publicClient,
  {
    account: "0x8eb02597d85abc268bc4769e06a0d4cc603ab05f",
    to: "0x4f93fa058b03953c851efaa2e4fc5c34afdfab84",
    value: "0x1",
    data: "0x",
  },
  STABLE_TOKEN_ADDRESS
);

// `estimateGasPrice` implemented above
const gasPrice = await estimateGasPrice(publicClient, STABLE_TOKEN_ADDRESS);

// Convert hex gas price to BigInt and calculate fees
const gasPriceBigInt = fromHex(gasPrice, "bigint");
const transactionFeesInCUSD = formatEther(gasLimit * gasPriceBigInt);

Resolve Minipay phone numbers to Addresses

Install the @celo/identity package:
npm install @celo/identity

Step 1: Set up your issuer

The issuer is the account registering attestations. When a user requests attestation registration, verify they own the identifier (e.g., SMS verification for phone numbers).
import { createWalletClient, http } from "viem";
import { celoSepolia } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";

// The issuer is the account that is registering the attestation
const ISSUER_PRIVATE_KEY = "YOUR_ISSUER_PRIVATE_KEY";

// Create Celo Sepolia viem client with the issuer private key
const viemClient = createWalletClient({
  account: privateKeyToAccount(ISSUER_PRIVATE_KEY),
  transport: http(),
  chain: celoSepolia,
});

// Information provided by user, issuer should confirm they own the identifier
const userPlaintextIdentifier = "+12345678910";
const userAccountAddress = "0x000000000000000000000000000000000000user";

// Time at which issuer verified the user owns their identifier
const attestationVerifiedTime = Date.now();

Step 2: Check and top up ODIS quota

import { OdisUtils } from "@celo/identity";
import { AuthSigner } from "@celo/identity/lib/odis/query";
import { OdisContextName } from "@celo/identity/lib/odis/query";

// authSigner provides information needed to authenticate with ODIS
const authSigner: AuthSigner = {
  authenticationMethod: OdisUtils.Query.AuthenticationMethod.WALLET_KEY,
  sign191: ({ message, account }) => viemClient.signMessage({ message, account }),
};

// serviceContext provides the ODIS endpoint and public key
const serviceContext = OdisUtils.Query.getServiceContext(
  OdisContextName.CELO_SEPOLIA
);

// Check existing quota on issuer account
const issuerAddress = viemClient.account.address;
const { remainingQuota } = await OdisUtils.Quota.getPnpQuotaStatus(
  issuerAddress,
  authSigner,
  serviceContext
);

// If needed, approve and send payment to OdisPayments to get quota for ODIS
// Note: This example uses viem. For contract interactions, use getContract from viem
if (remainingQuota < 1) {
  // Use viem's getContract to interact with stable token and ODIS payments contracts
  // Implementation depends on your specific contract setup
}

Step 3: Derive the obfuscated identifier

Get the obfuscated identifier from the plaintext identifier by querying ODIS:
const { obfuscatedIdentifier } = await OdisUtils.Identifier.getObfuscatedIdentifier(
  userPlaintextIdentifier,
  OdisUtils.Identifier.IdentifierPrefix.PHONE_NUMBER,
  issuerAddress,
  authSigner,
  serviceContext
);

Step 4: Look up account addresses

Query the FederatedAttestations contract to look up account addresses owned by an identifier:
const attestations = await federatedAttestationsContract.lookupAttestations(
  obfuscatedIdentifier,
  [issuerAddress] // Trusted issuers
);

console.log(attestations.accounts);

Request an ERC20 token transfer

import { createWalletClient, createPublicClient, custom, http, encodeFunctionData, parseUnits } from "viem";
import { celo, celoSepolia } from "viem/chains";
import { stableTokenABI } from "@celo/abis";

const walletClient = createWalletClient({
  chain: celoSepolia, // For testnet
  // chain: celo, // For mainnet
  transport: custom(window.ethereum!),
});

const publicClient = createPublicClient({
  chain: celoSepolia, // For testnet
  // chain: celo, // For mainnet
  transport: http(),
});

async function requestTransfer(tokenAddress, transferValue, tokenDecimals, receiverAddress) {
  const hash = await walletClient.sendTransaction({
    to: tokenAddress,
    // Mainnet addresses:
    // cUSD: '0x765DE816845861e75A25fCA122bb6898B8B1282a'
    // USDC: '0xcebA9300f2b948710d2653dD7B07f33A8B32118C'
    // USDT: '0x48065fbbe25f71c9282ddf5e1cd6d6a887483d5e'
    data: encodeFunctionData({
      abi: stableTokenABI, // Token ABI from @celo/abis
      functionName: "transfer",
      args: [
        receiverAddress,
        // Different tokens can have different decimals, cUSD (18), USDC (6)
        parseUnits(`${Number(transferValue)}`, tokenDecimals),
      ],
    }),
  });

  const transaction = await publicClient.waitForTransactionReceipt({
    hash, // Transaction hash that can be used to search transaction on the explorer.
  });

  if (transaction.status === "success") {
    // Do something after transaction is successful.
  } else {
    // Do something after transaction has failed.
  }
}