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)
}
}