Skip to content

Implementing a Custom Chain Signature Contract

This guide explains how to implement a custom Chain Signature Contract to provide as argument when instantiating a Chain instance.

Overview

The ChainSignatureContract is an abstract class that defines the interface for interacting with the Sig Network infrastructure.

  • Key Derivation: Derive the child key from the root key using Sig Network key derivation function
  • Sign: Sigs the given payload using Sig Network MPC
  • Current Fee: Gets the current signature deposit that handles network congestion
  • Root Public Key: Gets the root public key of the Smart Contract on Sig Network MPC

While the library includes a default implementation for the NEAR protocol, you have the flexibility to implement your own contract for other blockchain networks.

Using the NEAR Implementation

import { utils, EVM } from 'signet.js'
 
const contract = new utils.chains.near.ChainSignatureContract({
  networkId: 'testnet',
  contractId: 'v1.signer-prod.testnet',
  accountId,
  keypair,
})
 
const evmChain = new EVM({
  rpcUrl: 'https://mainnet.infura.io/v3/YOUR-PROJECT-ID',
  contract,
})

Implementing a Custom Chain Signature Contract

To create your own implementation to use on the Chain Instance, your contract must implement the BaseChainSignatureContract interface.

In case you need you want all Sig Network Smart Contract capabilities, you can implement the ChainSignatureContract interface.

import type BN from 'bn.js'
 
import type { RSVSignature, UncompressedPubKeySEC1 } from '@chains/types'
 
export interface SignArgs {
  /** The payload to sign as an array of 32 bytes */
  payload: number[]
  /** The derivation path for key generation */
  path: string
  /** Version of the key to use */
  key_version: number
}
 
/**
 * Base contract interface required for compatibility with Chain instances like EVM and Bitcoin.
 *
 * See {@link EVM} and {@link Bitcoin} for example implementations.
 */
export abstract class BaseChainSignatureContract {
  /**
   * Gets the current signature deposit required by the contract.
   * This deposit amount helps manage network congestion.
   *
   * @returns Promise resolving to the required deposit amount as a BigNumber
   */
  abstract getCurrentSignatureDeposit(): Promise<BN>
 
  /**
   * Derives a child public key using a\ derivation path and predecessor.
   *
   * @param args - Arguments for key derivation
   * @param args.path - The string path to use derive the key
   * @param args.predecessor - The id/address of the account requesting signature
   * @returns Promise resolving to the derived SEC1 uncompressed public key
   */
  abstract getDerivedPublicKey(
    args: {
      path: string
      predecessor: string
    } & Record<string, unknown>
  ): Promise<UncompressedPubKeySEC1>
}
 
/**
 * Full contract interface that extends BaseChainSignatureContract to provide all Sig Network Smart Contract capabilities.
 */
export abstract class ChainSignatureContract extends BaseChainSignatureContract {
  /**
   * Signs a payload using Sig Network MPC.
   *
   * @param args - Arguments for the signing operation
   * @param args.payload - The data to sign as an array of 32 bytes
   * @param args.path - The string path to use derive the key
   * @param args.key_version - Version of the key to use
   * @returns Promise resolving to the RSV signature
   */
  abstract sign(args: SignArgs & Record<string, unknown>): Promise<RSVSignature>
 
  /**
   * Gets the public key associated with this contract instance.
   *
   * @returns Promise resolving to the SEC1 uncompressed public key
   */
  abstract getPublicKey(): Promise<UncompressedPubKeySEC1>
}

Example: NEAR Implementation

Below is the reference implementation for the NEAR, which you can use as a guide for implementing your own chain signature contract:

import { Contract } from '@near-js/accounts'
import { KeyPair } from '@near-js/crypto'
import BN from 'bn.js'
 
import { ChainSignatureContract as AbstractChainSignatureContract } from '@chains/ChainSignatureContract'
import type { SignArgs } from '@chains/ChainSignatureContract'
import type {
  RSVSignature,
  MPCSignature,
  UncompressedPubKeySEC1,
  NajPublicKey,
} from '@chains/types'
import { cryptography } from '@utils'
import { getNearAccount } from '@utils/chains/near/account'
import {
  DONT_CARE_ACCOUNT_ID,
  NEAR_MAX_GAS,
} from '@utils/chains/near/constants'
import {
  type NearNetworkIds,
  type ChainSignatureContractIds,
} from '@utils/chains/near/types'
import { najToUncompressedPubKeySEC1 } from '@utils/cryptography'
 
const requireAccount = (accountId: string): void => {
  if (accountId === DONT_CARE_ACCOUNT_ID) {
    throw new Error(
      'A valid account ID and keypair are required for change methods. Please instantiate a new contract with valid credentials.'
    )
  }
}
 
type NearContract = Contract & {
  public_key: () => Promise<NajPublicKey>
  sign: (args: {
    args: { request: SignArgs }
    gas: BN
    amount: BN
  }) => Promise<MPCSignature>
  experimental_signature_deposit: () => Promise<number>
  derived_public_key: (args: {
    path: string
    predecessor: string
  }) => Promise<NajPublicKey>
}
 
interface ChainSignatureContractArgs {
  networkId: NearNetworkIds
  contractId: ChainSignatureContractIds
  accountId?: string
  keypair?: KeyPair
}
 
/**
 * Implementation of the ChainSignatureContract for NEAR chains.
 *
 * This class provides an interface to interact with the ChainSignatures contract
 * deployed on NEAR. It supports both view methods (which don't require authentication)
 * and change methods (which require a valid NEAR account and keypair).
 *
 * @extends AbstractChainSignatureContract
 */
export class ChainSignatureContract extends AbstractChainSignatureContract {
  private readonly networkId: NearNetworkIds
  private readonly contractId: ChainSignatureContractIds
  private readonly accountId: string
  private readonly keypair: KeyPair
 
  /**
   * Creates a new instance of the ChainSignatureContract for NEAR chains.
   *
   * @param args - Configuration options for the contract
   * @param args.networkId - The NEAR network ID (e.g. 'testnet', 'mainnet')
   * @param args.contractId - The contract ID of the deployed ChainSignatures contract
   * @param args.accountId - Optional NEAR account ID for signing transactions. Required for change methods.
   * @param args.keypair - Optional NEAR KeyPair for signing transactions. Required for change methods.
   */
  constructor({
    networkId,
    contractId,
    accountId = DONT_CARE_ACCOUNT_ID,
    keypair = KeyPair.fromRandom('ed25519'),
  }: ChainSignatureContractArgs) {
    // TODO: Should use the hardcoded ROOT_PUBLIC_KEY as in the EVM ChainSignatureContract
    super()
 
    this.networkId = networkId
    this.contractId = contractId
    this.accountId = accountId
    this.keypair = keypair
  }
 
  private async getContract(): Promise<NearContract> {
    const account = await getNearAccount({
      networkId: this.networkId,
      accountId: this.accountId,
      keypair: this.keypair,
    })
 
    return new Contract(account, this.contractId, {
      viewMethods: [
        'public_key',
        'experimental_signature_deposit',
        'derived_public_key',
      ],
      changeMethods: ['sign'],
      useLocalViewExecution: false,
    }) as unknown as NearContract
  }
 
  async getCurrentSignatureDeposit(): Promise<BN> {
    const contract = await this.getContract()
    return new BN(
      (await contract.experimental_signature_deposit()).toLocaleString(
        'fullwide',
        {
          useGrouping: false,
        }
      )
    )
  }
 
  async getDerivedPublicKey(args: {
    path: string
    predecessor: string
  }): Promise<UncompressedPubKeySEC1> {
    const contract = await this.getContract()
 
    const najPubKey = await contract.derived_public_key(args)
    return najToUncompressedPubKeySEC1(najPubKey)
  }
 
  async getPublicKey(): Promise<UncompressedPubKeySEC1> {
    const contract = await this.getContract()
 
    const najPubKey = await contract.public_key()
    return najToUncompressedPubKeySEC1(najPubKey)
  }
 
  // TODO: Should call the contract without the Contract instance as it doesn't allow for proper timeout handling on the BE
  async sign(args: SignArgs): Promise<RSVSignature> {
    requireAccount(this.accountId)
 
    const contract = await this.getContract()
    const deposit = await this.getCurrentSignatureDeposit()
 
    const signature = await contract.sign({
      args: { request: args },
      gas: NEAR_MAX_GAS,
      amount: deposit,
    })
 
    return cryptography.toRSV(signature)
  }
}