Deploy and test the multi-signature wallet
This test of the multi-signature wallet does the following:
- Implements a special multi-signature signer and a 'helper' class representing the multi-signature wallet
- Creates two new 'regular' wallets and uses one of them to deploy the multi-signature wallet
- Creates a message signed by both wallets and sends it to the multi-signature wallet
Import statements
import { createRequire } from "node:module";
const require = createRequire(import.meta.url);
import {
Faucet,
HttpTransport,
LocalECDSAKeySigner,
PublicClient,
WalletV1,
generateRandomPrivateKey,
convertEthToWei,
waitTillCompleted,
bytesToHex,
externalDeploymentMessage,
ExternalMessageEnvelope,
hexToBytes,
isHexString,
refineAddress,
type ISigner,
type Hex,
} from "@nilfoundation/niljs";
import { secp256k1 } from "@noble/curves/secp256k1";
import {} from "ethers";
import { type Abi, encodeFunctionData } from "viem";
import { concatBytes, numberToBytesBE } from "@noble/curves/abstract/utils";
Auxiliary functions
The refineFunctionHexData()
function is a special 'helper' function that encodes calldata after passing several checks:
const refineFunctionHexData = ({
data,
abi,
functionName,
args,
}: {
data?: Uint8Array | Hex;
abi?: Abi;
functionName?: string;
args?: unknown[];
}): Hex => {
if (!data && !abi) {
return "0x";
}
if (data) {
return typeof data === "string" ? data : bytesToHex(data);
}
if (!functionName) {
throw new Error("Function name is required");
}
if (!abi) {
throw new Error("ABI is required");
}
return encodeFunctionData({
abi,
functionName: functionName,
args: args || [],
});
};
The multi-signature signer and wallet
The MultisigSigner
class creates multi-signatures given an array of keys while the MultiSigWallet
class provides an abstraction over sending external messages to the multi-signature wallet contracts:
/**
* MultisigSigner is a special signer that can create an array of signatures
* when given a the data to sign.
*
* @class MultisigSigner
* @typedef {MultisigSigner}
* @implements {ISigner}
*/
class MultisigSigner implements ISigner {
private keys: Uint8Array[];
constructor(keys: Uint8Array[]) {
for (let i = 0; i < keys.length; i++) {
if (keys[i].length !== 32) {
throw new Error("Invalid key length");
}
}
this.keys = keys;
}
async sign(data: Uint8Array): Promise<Uint8Array> {
const fullSignatures = new Uint8Array(this.keys.length * 65);
for (let i = 0; i < this.keys.length; i++) {
const signature = secp256k1.sign(data, this.keys[i]);
const { r, s, recovery } = signature;
fullSignatures.set(
concatBytes(
numberToBytesBE(r, 32),
numberToBytesBE(s, 32),
numberToBytesBE(recovery, 1),
),
i * 65,
);
}
return fullSignatures;
}
getPublicKey(params: unknown): Promise<Uint8Array> {
throw new Error("Method not implemented.");
}
getAddress(params: unknown): Promise<Uint8Array> {
throw new Error("Method not implemented.");
}
}
/**
* MultiSigWallet is a 'helper' class for sending external messages
* to the multi-signature wallet.
*
* @class MultiSigWallet
* @typedef {MultiSigWallet}
*/
class MultiSigWallet {
private keys: Uint8Array[];
private salt: bigint;
private chainId: number;
private client: PublicClient;
public address: Hex;
constructor(
keys: (Uint8Array | Hex)[],
salt: bigint,
chainId: number,
shardId: number,
client: PublicClient,
) {
this.keys = keys.map((key) => {
if (isHexString(key)) {
return hexToBytes(key);
}
return key;
});
this.salt = salt;
this.address = MultiSigWallet.calculateAddress(chainId, shardId, keys, salt);
this.chainId = chainId;
this.client = client;
}
static calculateAddress(
chainId: number,
shardId: number,
keys: (Uint8Array | Hex)[],
salt: bigint,
) {
const msg = externalDeploymentMessage(
{
abi: MULTISIG_WALLET_ABI,
args: [keys],
bytecode: MULTISIG_WALLET_BYTECODE,
salt,
shard: shardId,
},
chainId,
);
return msg.hexAddress();
}
async sendTransaction({
to,
refundTo,
bounceTo,
data,
abi,
functionName,
args,
deploy,
seqno,
feeCredit,
value,
tokens,
chainId,
}: SendMessageParams) {
const refinedSeqno = seqno ?? (await this.client.getMessageCount(this.address, "latest"));
const hexTo = bytesToHex(refineAddress(to));
const hexRefundTo = bytesToHex(refineAddress(refundTo ?? this.address));
const hexBounceTo = bytesToHex(refineAddress(bounceTo ?? this.address));
const hexData = refineFunctionHexData({ data, abi, functionName, args });
const callData = encodeFunctionData({
abi: MULTISIG_WALLET_ABI,
functionName: "asyncCall",
args: [hexTo, hexRefundTo, hexBounceTo, feeCredit, tokens ?? [], value ?? 0n, hexData],
});
const msg = new ExternalMessageEnvelope({
isDeploy: !!deploy,
data: hexToBytes(callData),
to: hexToBytes(this.address),
seqno: refinedSeqno,
chainId: chainId ?? this.chainId,
authData: new Uint8Array(0),
});
const { raw } = await msg.encodeWithSignature(signer);
const hash = await this.client.sendRawMessage(raw);
return hash;
}
}