@runonflux/aa-schnorr-multisig-sdk
Version:
Account Abstraction Schnorr Multi-Signatures SDK
257 lines (256 loc) • 11.3 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.MultiSigUserOp = void 0;
const ajv_1 = __importDefault(require("ajv"));
const ethers_1 = require("ethers");
const secp256k1_1 = require("secp256k1");
const types_1 = require("../types");
const serializers_1 = require("../serializers");
const schnorr_helpers_1 = require("../helpers/schnorr-helpers");
const converters_1 = require("../helpers/converters");
const signers_1 = require("../signers");
const errors_1 = require("../errors");
class MultiSigUserOp {
constructor(publicKeys, publicNonces, opHash, userOpRequest) {
this.publicNonces = {};
this.publicKeys = {};
this.signatures = {};
if (publicKeys.length < 2)
throw new Error("At least 2 signers should be provided");
this.opHash = opHash;
this.userOpRequest = userOpRequest;
// map public keys and public nonces
const _publicKeys = publicKeys.map((pk, index) => {
const _address = (0, converters_1.pubKey2Address)(Buffer.from((0, secp256k1_1.publicKeyConvert)(pk.buffer, false)));
this.publicNonces[_address] = publicNonces[index];
this.publicKeys[_address] = pk;
return pk;
});
// get combined public key created from all signers' public keys
const _combinedPubKey = signers_1.Schnorrkel.getCombinedPublicKey(_publicKeys);
this.combinedPubKey = _combinedPubKey;
// create unique tx id
const _salt = Buffer.from(ethers_1.ethers.randomBytes(32));
const coder = new ethers_1.AbiCoder();
const encodedParams = coder.encode(["bytes", "bytes", "bytes"], [_combinedPubKey.buffer, opHash, _salt]);
this.id = ethers_1.ethers.keccak256(encodedParams);
}
getOpHash() {
return this.opHash;
}
signMultiSigHash(signer) {
const op = this.opHash;
const pk = this._getPublicKeys();
const pn = this._getPublicNonces();
const _signatureOutput = signer.signMultiSigHash(op, pk, pn);
this.signatures[signer.getAddress()] = _signatureOutput;
return _signatureOutput;
}
getSummedSigData() {
if (!this.combinedPubKey || !this.signatures)
throw new Error("Summed signature input data is missing");
const _signatureOutputs = this._getSignatures();
const _sigs = _signatureOutputs.map((sig) => sig.signature);
const _challenges = _signatureOutputs.map((sig) => sig.challenge);
// sum all signers signatures
const _summed = (0, schnorr_helpers_1.sumSchnorrSigs)(_sigs);
// challenge for every signature must be the same - check and if so, assign first one
const isEveryChallengeEqual = _challenges.every((e) => {
if (e.toHex() === _challenges[0].toHex())
return true;
});
if (!isEveryChallengeEqual)
throw new Error("Challenges for all signers should be the same");
const e = _challenges[0];
// the multisig px and parity
const px = ethers_1.ethers.hexlify(this.combinedPubKey.buffer.subarray(1, 33));
const parity = this.combinedPubKey.buffer[0] - 2 + 27;
// wrap the result
const abiCoder = new ethers_1.AbiCoder();
const sigData = abiCoder.encode(["bytes32", "bytes32", "bytes32", "uint8"], [px, e.buffer, _summed.buffer, parity]);
return sigData;
}
getAddressSignature(signerAddress) {
return this._getSignatures()[signerAddress];
}
getAddressPublicNonces(signerAddress) {
return this._getPublicNonces()[signerAddress];
}
getAddressPubKeys(signerAddress) {
return this._getPublicKeys()[signerAddress];
}
_getSignatures() {
return Object.entries(this.signatures).map(([, sig]) => {
return sig;
});
}
_getPublicNonces() {
return Object.entries(this.publicNonces).map(([, nonce]) => {
return nonce;
});
}
_getPublicKeys() {
return Object.entries(this.publicKeys).map(([, pk]) => {
return pk;
});
}
toJson() {
return {
id: this.id,
opHash: this.opHash,
userOpRequest: {
sender: this.userOpRequest.sender,
nonce: new serializers_1.BigNumberSerializer(this.userOpRequest.nonce).toString(),
initCode: this.userOpRequest.initCode.toString(),
callData: this.userOpRequest.callData.toString(),
callGasLimit: new serializers_1.BigNumberSerializer(this.userOpRequest.callGasLimit).toString(),
verificationGasLimit: new serializers_1.BigNumberSerializer(this.userOpRequest.verificationGasLimit).toString(),
preVerificationGas: new serializers_1.BigNumberSerializer(this.userOpRequest.preVerificationGas).toString(),
maxFeePerGas: new serializers_1.BigNumberSerializer(this.userOpRequest.maxFeePerGas).toString(),
maxPriorityFeePerGas: new serializers_1.BigNumberSerializer(this.userOpRequest.maxPriorityFeePerGas).toString(),
paymasterAndData: this.userOpRequest.paymasterAndData.toString(),
signature: this.userOpRequest.signature.toString(),
},
combinedPubKey: this.combinedPubKey.toHex(),
publicKeys: Object.fromEntries(Object.entries(this.publicKeys).map(([address, key]) => [address, key.toHex()])),
publicNonces: Object.fromEntries(Object.entries(this.publicNonces).map(([address, nonces]) => [
address,
{
kPublic: nonces.kPublic.toHex(),
kTwoPublic: nonces.kTwoPublic.toHex(),
},
])),
signatures: Object.fromEntries(Object.entries(this.signatures).map(([address, output]) => [
address,
{
finalPublicNonce: output.finalPublicNonce.toHex(),
challenge: output.challenge.toHex(),
signature: output.signature.toHex(),
},
])),
};
}
}
exports.MultiSigUserOp = MultiSigUserOp;
MultiSigUserOp.fromJson = (serialized) => {
const schema = {
type: "object",
properties: {
id: { type: "string" },
opHash: { type: "string" },
userOpRequest: {
type: "object",
properties: {
sender: { type: "string" },
nonce: { type: "string" },
initCode: { type: "string" },
callData: { type: "string" },
callGasLimit: { type: "string" },
verificationGasLimit: { type: "string" },
preVerificationGas: { type: "string" },
maxFeePerGas: { type: "string" },
maxPriorityFeePerGas: { type: "string" },
paymasterAndData: { type: "string" },
signature: { type: "string" },
},
required: [
"sender",
"nonce",
"initCode",
"callData",
"callGasLimit",
"verificationGasLimit",
"preVerificationGas",
"maxFeePerGas",
"maxPriorityFeePerGas",
"paymasterAndData",
"signature",
],
},
combinedPubKey: { type: "string" },
publicNonces: {
type: "object",
patternProperties: {
".*": {
type: "object",
properties: {
kPublic: { type: "string" },
kTwoPublic: { type: "string" },
},
required: ["kPublic", "kTwoPublic"],
},
},
},
publicKeys: {
type: "object",
patternProperties: {
".*": { type: "string" },
},
},
signatures: {
type: "object",
patternProperties: {
".*": {
type: "object",
properties: {
finalPublicNonce: { type: "string" },
challenge: { type: "string" },
signature: { type: "string" },
},
required: ["finalPublicNonce", "challenge", "signature"],
},
},
},
},
};
const ajv = new ajv_1.default();
const validate = ajv.compile(schema);
const isValid = validate(serialized);
if (!isValid)
throw new errors_1.ValidationError("[MultiSigUserOP]: Invalid JSON format", validate.errors);
const { id } = serialized;
const { opHash } = serialized;
const userOpRequest = {
sender: serialized.userOpRequest.sender,
nonce: serializers_1.BigNumberSerializer.fromString(serialized.userOpRequest.nonce).number,
initCode: serialized.userOpRequest.initCode,
callData: serialized.userOpRequest.callData,
callGasLimit: serialized.userOpRequest.callGasLimit,
verificationGasLimit: serialized.userOpRequest.verificationGasLimit,
preVerificationGas: serializers_1.BigNumberSerializer.fromString(serialized.userOpRequest.preVerificationGas).number,
maxFeePerGas: serializers_1.BigNumberSerializer.fromString(serialized.userOpRequest.maxFeePerGas).number,
maxPriorityFeePerGas: serializers_1.BigNumberSerializer.fromString(serialized.userOpRequest.maxPriorityFeePerGas).number,
paymasterAndData: serialized.userOpRequest.paymasterAndData,
signature: serialized.userOpRequest.signature,
};
const combinedPubKey = types_1.Key.fromHex(serialized.combinedPubKey);
const publicNonces = Object.fromEntries(Object.entries(serialized.publicNonces).map(([address, nonces]) => [
address,
{
kPublic: types_1.Key.fromHex(nonces.kPublic),
kTwoPublic: types_1.Key.fromHex(nonces.kTwoPublic),
},
]));
const publicKeys = Object.fromEntries(Object.entries(serialized.publicKeys).map(([address, key]) => [address, types_1.Key.fromHex(key)]));
const signatures = Object.fromEntries(Object.entries(serialized.signatures).map(([address, output]) => [
address,
{
finalPublicNonce: types_1.Key.fromHex(output.finalPublicNonce),
challenge: types_1.Key.fromHex(output.challenge),
signature: types_1.Key.fromHex(output.signature),
},
]));
const instance = Object.create(MultiSigUserOp.prototype);
return Object.assign(instance, {
id,
opHash,
userOpRequest,
combinedPubKey,
publicNonces,
publicKeys,
signatures,
});
};