mirror of
https://github.com/alexgo-io/stacks.js.git
synced 2026-01-12 17:52:41 +08:00
feat: add multisig support to contract deploys (#1539)
* feat: multisig support for contract deploy transactions * fix: contract deploy options type removed * chore: add backward compatibility * test: add origin verification to tests --------- Co-authored-by: janniks <6362150+janniks@users.noreply.github.com> Co-authored-by: janniks <janniks@users.noreply.github.com>
This commit is contained in:
@@ -18,7 +18,7 @@ import {
|
||||
ClarityAbi,
|
||||
ClarityValue,
|
||||
ContractCallPayload,
|
||||
ContractDeployOptions,
|
||||
SignedContractDeployOptions,
|
||||
createStacksPrivateKey,
|
||||
cvToString,
|
||||
estimateContractDeploy,
|
||||
@@ -766,7 +766,7 @@ async function contractDeploy(network: CLINetworkAdapter, args: string[]): Promi
|
||||
? new StacksMainnet({ url: network.legacyNetwork.blockstackAPIUrl })
|
||||
: new StacksTestnet({ url: network.legacyNetwork.blockstackAPIUrl });
|
||||
|
||||
const options: ContractDeployOptions = {
|
||||
const options: SignedContractDeployOptions = {
|
||||
contractName,
|
||||
codeBody: source,
|
||||
senderKey: privateKey,
|
||||
|
||||
@@ -38,7 +38,6 @@ import { ClarityAbi, validateContractCall } from './contract-abi';
|
||||
import { NoEstimateAvailableError } from './errors';
|
||||
import {
|
||||
createStacksPrivateKey,
|
||||
createStacksPublicKey,
|
||||
getPublicKey,
|
||||
pubKeyfromPrivKey,
|
||||
publicKeyFromBytes,
|
||||
@@ -707,16 +706,29 @@ export interface BaseContractDeployOptions {
|
||||
sponsored?: boolean;
|
||||
}
|
||||
|
||||
export interface ContractDeployOptions extends BaseContractDeployOptions {
|
||||
/** a hex string of the private key of the transaction sender */
|
||||
senderKey: string;
|
||||
}
|
||||
|
||||
export interface UnsignedContractDeployOptions extends BaseContractDeployOptions {
|
||||
/** a hex string of the public key of the transaction sender */
|
||||
publicKey: string;
|
||||
}
|
||||
|
||||
export interface SignedContractDeployOptions extends BaseContractDeployOptions {
|
||||
senderKey: string;
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link SignedContractDeployOptions} or {@link UnsignedContractDeployOptions} instead. */
|
||||
export interface ContractDeployOptions extends SignedContractDeployOptions {}
|
||||
|
||||
export interface UnsignedMultiSigContractDeployOptions extends BaseContractDeployOptions {
|
||||
numSignatures: number;
|
||||
publicKeys: string[];
|
||||
}
|
||||
|
||||
export interface SignedMultiSigContractDeployOptions extends BaseContractDeployOptions {
|
||||
numSignatures: number;
|
||||
publicKeys: string[];
|
||||
signerKeys: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use the new {@link estimateTransaction} function insterad.
|
||||
*
|
||||
@@ -772,31 +784,49 @@ export async function estimateContractDeploy(
|
||||
/**
|
||||
* Generates a Clarity smart contract deploy transaction
|
||||
*
|
||||
* @param {ContractDeployOptions} txOptions - an options object for the contract deploy
|
||||
* @param {SignedContractDeployOptions | SignedMultiSigContractDeployOptions} txOptions - an options object for the contract deploy
|
||||
*
|
||||
* Returns a signed Stacks smart contract deploy transaction.
|
||||
*
|
||||
* @return {StacksTransaction}
|
||||
*/
|
||||
export async function makeContractDeploy(
|
||||
txOptions: ContractDeployOptions
|
||||
txOptions: SignedContractDeployOptions | SignedMultiSigContractDeployOptions
|
||||
): Promise<StacksTransaction> {
|
||||
const privKey = createStacksPrivateKey(txOptions.senderKey);
|
||||
const stacksPublicKey = getPublicKey(privKey);
|
||||
const publicKey = publicKeyToString(stacksPublicKey);
|
||||
const unsignedTxOptions: UnsignedContractDeployOptions = { ...txOptions, publicKey };
|
||||
const transaction: StacksTransaction = await makeUnsignedContractDeploy(unsignedTxOptions);
|
||||
if ('senderKey' in txOptions) {
|
||||
// txOptions is SignedContractDeployOptions
|
||||
const publicKey = publicKeyToString(getPublicKey(createStacksPrivateKey(txOptions.senderKey)));
|
||||
const options = omit(txOptions, 'senderKey');
|
||||
const transaction = await makeUnsignedContractDeploy({ publicKey, ...options });
|
||||
|
||||
if (txOptions.senderKey) {
|
||||
const privKey = createStacksPrivateKey(txOptions.senderKey);
|
||||
const signer = new TransactionSigner(transaction);
|
||||
signer.signOrigin(privKey);
|
||||
}
|
||||
|
||||
return transaction;
|
||||
return transaction;
|
||||
} else {
|
||||
// txOptions is SignedMultiSigContractDeployOptions
|
||||
const options = omit(txOptions, 'signerKeys');
|
||||
const transaction = await makeUnsignedContractDeploy(options);
|
||||
|
||||
const signer = new TransactionSigner(transaction);
|
||||
let pubKeys = txOptions.publicKeys;
|
||||
for (const key of txOptions.signerKeys) {
|
||||
const pubKey = pubKeyfromPrivKey(key);
|
||||
pubKeys = pubKeys.filter(pk => pk !== bytesToHex(pubKey.data));
|
||||
signer.signOrigin(createStacksPrivateKey(key));
|
||||
}
|
||||
|
||||
for (const key of pubKeys) {
|
||||
signer.appendOrigin(publicKeyFromBytes(hexToBytes(key)));
|
||||
}
|
||||
|
||||
return transaction;
|
||||
}
|
||||
}
|
||||
|
||||
export async function makeUnsignedContractDeploy(
|
||||
txOptions: UnsignedContractDeployOptions
|
||||
txOptions: UnsignedContractDeployOptions | UnsignedMultiSigContractDeployOptions
|
||||
): Promise<StacksTransaction> {
|
||||
const defaultOptions = {
|
||||
fee: BigInt(0),
|
||||
@@ -815,17 +845,28 @@ export async function makeUnsignedContractDeploy(
|
||||
options.clarityVersion
|
||||
);
|
||||
|
||||
const addressHashMode = AddressHashMode.SerializeP2PKH;
|
||||
const pubKey = createStacksPublicKey(options.publicKey);
|
||||
|
||||
let authorization: Authorization | null = null;
|
||||
|
||||
const spendingCondition = createSingleSigSpendingCondition(
|
||||
addressHashMode,
|
||||
publicKeyToString(pubKey),
|
||||
options.nonce,
|
||||
options.fee
|
||||
);
|
||||
let spendingCondition: SpendingCondition | null = null;
|
||||
|
||||
if ('publicKey' in options) {
|
||||
// single-sig
|
||||
spendingCondition = createSingleSigSpendingCondition(
|
||||
AddressHashMode.SerializeP2PKH,
|
||||
options.publicKey,
|
||||
options.nonce,
|
||||
options.fee
|
||||
);
|
||||
} else {
|
||||
// multi-sig
|
||||
spendingCondition = createMultiSigSpendingCondition(
|
||||
AddressHashMode.SerializeP2SH,
|
||||
options.numSignatures,
|
||||
options.publicKeys,
|
||||
options.nonce,
|
||||
options.fee
|
||||
);
|
||||
}
|
||||
|
||||
if (options.sponsored) {
|
||||
authorization = createSponsoredAuth(spendingCondition);
|
||||
@@ -863,7 +904,7 @@ export async function makeUnsignedContractDeploy(
|
||||
options.network.version === TransactionVersion.Mainnet
|
||||
? AddressVersion.MainnetSingleSig
|
||||
: AddressVersion.TestnetSingleSig;
|
||||
const senderAddress = publicKeyToAddress(addressVersion, pubKey);
|
||||
const senderAddress = c32address(addressVersion, transaction.auth.spendingCondition!.signer);
|
||||
const txNonce = await getNonce(senderAddress, options.network);
|
||||
transaction.setNonce(txNonce);
|
||||
}
|
||||
|
||||
@@ -183,6 +183,7 @@ test('Make STX token transfer with set tx fee', async () => {
|
||||
memo,
|
||||
anchorMode: AnchorMode.Any,
|
||||
});
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
|
||||
const serialized = bytesToHex(transaction.serialize());
|
||||
|
||||
@@ -241,10 +242,10 @@ test('Make STX token transfer with fee estimate', async () => {
|
||||
anchorMode: AnchorMode.Any,
|
||||
});
|
||||
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
expect(transaction.auth.spendingCondition?.fee?.toString()).toEqual('180');
|
||||
|
||||
const serialized = bytesToHex(transaction.serialize());
|
||||
|
||||
const tx =
|
||||
'0000000001040015c31b8c1c11c515e244b75806bac48d1399c775000000000000000000000000000000b4' +
|
||||
'0001e5ac1152f6018fbfded102268b22086666150823d0ae57f4023bde058a7ff0b279076db25b358b8833' +
|
||||
@@ -275,9 +276,9 @@ test('Make STX token transfer with testnet', async () => {
|
||||
memo: memo,
|
||||
anchorMode: AnchorMode.Any,
|
||||
});
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
|
||||
const serialized = bytesToHex(transaction.serialize());
|
||||
|
||||
const tx =
|
||||
'8080000000040015c31b8c1c11c515e244b75806bac48d1399c77500000000000000000000000000000000' +
|
||||
'00014199f63f7e010141a36a4624d032758f54e08ff03b24ed2667463eb405b4d81505631b32a1f13b5737' +
|
||||
@@ -299,9 +300,9 @@ test('Make STX token transfer with testnet string name', async () => {
|
||||
memo: 'test memo',
|
||||
anchorMode: AnchorMode.Any,
|
||||
});
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
|
||||
const serialized = bytesToHex(transaction.serialize());
|
||||
|
||||
const tx =
|
||||
'8080000000040015c31b8c1c11c515e244b75806bac48d1399c77500000000000000000000000000000000' +
|
||||
'00014199f63f7e010141a36a4624d032758f54e08ff03b24ed2667463eb405b4d81505631b32a1f13b5737' +
|
||||
@@ -395,6 +396,7 @@ test('Make Multi-Sig STX token transfer', async () => {
|
||||
signer.signOrigin(privKeys[0]);
|
||||
signer.signOrigin(privKeys[1]);
|
||||
signer.appendOrigin(pubKeys[2]);
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
|
||||
const serializedTx = transaction.serialize();
|
||||
const tx =
|
||||
@@ -644,6 +646,7 @@ test('Make Multi-Sig STX token transfer with two transaction signers', async ()
|
||||
|
||||
const bytesReader = new BytesReader(serializedTx);
|
||||
const deserializedTx = deserializeTransaction(bytesReader);
|
||||
expect(() => deserializedTx.verifyOrigin()).not.toThrow();
|
||||
|
||||
expect(deserializedTx.auth.authType).toBe(authType);
|
||||
|
||||
@@ -723,6 +726,7 @@ test('Make versioned smart contract deploy', async () => {
|
||||
anchorMode: AnchorMode.Any,
|
||||
clarityVersion: ClarityVersion.Clarity2,
|
||||
});
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
|
||||
const serialized = bytesToHex(transaction.serialize());
|
||||
|
||||
@@ -748,6 +752,7 @@ test('Make smart contract deploy (defaults to versioned smart contract, as of 2.
|
||||
network: new StacksTestnet(),
|
||||
anchorMode: AnchorMode.Any,
|
||||
});
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
|
||||
const serialized = bytesToHex(transaction.serialize());
|
||||
|
||||
@@ -767,6 +772,7 @@ test('Make smart contract deploy with network string name (defaults to versioned
|
||||
network: 'testnet',
|
||||
anchorMode: AnchorMode.Any,
|
||||
});
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
|
||||
const serialized = bytesToHex(transaction.serialize());
|
||||
|
||||
@@ -807,6 +813,37 @@ test('Make smart contract deploy unsigned', async () => {
|
||||
expect(deserializedTx.auth.spendingCondition!.fee!.toString()).toBe(fee.toString());
|
||||
});
|
||||
|
||||
test('make a multi-sig contract deploy', async () => {
|
||||
const contractName = 'kv-store';
|
||||
const codeBody = fs.readFileSync('./tests/contracts/kv-store.clar').toString();
|
||||
const fee = 0;
|
||||
const nonce = 0;
|
||||
const privKeyStrings = [
|
||||
'6d430bb91222408e7706c9001cfaeb91b08c2be6d5ac95779ab52c6b431950e001',
|
||||
'2a584d899fed1d24e26b524f202763c8ab30260167429f157f1c119f550fa6af01',
|
||||
'd5200dee706ee53ae98a03fba6cf4fdcc5084c30cfa9e1b3462dcdeaa3e0f1d201',
|
||||
];
|
||||
|
||||
const pubKeys = privKeyStrings.map(pubKeyfromPrivKey);
|
||||
const pubKeyStrings = pubKeys.map(publicKeyToString);
|
||||
|
||||
const transaction = await makeContractDeploy({
|
||||
codeBody,
|
||||
contractName,
|
||||
publicKeys: pubKeyStrings,
|
||||
numSignatures: 3,
|
||||
signerKeys: privKeyStrings,
|
||||
fee,
|
||||
nonce,
|
||||
network: new StacksTestnet(),
|
||||
anchorMode: AnchorMode.Any,
|
||||
});
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
expect(transaction.auth.spendingCondition!.signer).toEqual(
|
||||
'04128cacf0764f69b1e291f62d1dcdd8f65be5ab'
|
||||
);
|
||||
});
|
||||
|
||||
test('Make smart contract deploy signed', async () => {
|
||||
const contractName = 'kv-store';
|
||||
const codeBody = fs.readFileSync('./tests/contracts/kv-store.clar').toString();
|
||||
@@ -825,6 +862,7 @@ test('Make smart contract deploy signed', async () => {
|
||||
network: new StacksTestnet(),
|
||||
anchorMode: AnchorMode.Any,
|
||||
});
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
|
||||
const serializedTx = transaction.serialize();
|
||||
|
||||
@@ -857,6 +895,7 @@ test('Make contract-call', async () => {
|
||||
network: new StacksTestnet(),
|
||||
anchorMode: AnchorMode.Any,
|
||||
});
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
|
||||
const serialized = bytesToHex(transaction.serialize());
|
||||
|
||||
@@ -881,6 +920,7 @@ test('Make contract-call with network string', async () => {
|
||||
network: 'testnet',
|
||||
anchorMode: AnchorMode.Any,
|
||||
});
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
|
||||
const serialized = bytesToHex(transaction.serialize());
|
||||
|
||||
@@ -952,6 +992,7 @@ test('Make contract-call with post conditions', async () => {
|
||||
postConditionMode: PostConditionMode.Deny,
|
||||
anchorMode: AnchorMode.Any,
|
||||
});
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
|
||||
const serialized = bytesToHex(transaction.serialize());
|
||||
|
||||
@@ -996,6 +1037,7 @@ test('Make contract-call with post condition allow mode', async () => {
|
||||
postConditionMode: PostConditionMode.Allow,
|
||||
anchorMode: AnchorMode.Any,
|
||||
});
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
|
||||
const serialized = bytesToHex(transaction.serialize());
|
||||
|
||||
@@ -1055,12 +1097,11 @@ test('make a multi-sig contract call', async () => {
|
||||
'2a584d899fed1d24e26b524f202763c8ab30260167429f157f1c119f550fa6af01',
|
||||
'd5200dee706ee53ae98a03fba6cf4fdcc5084c30cfa9e1b3462dcdeaa3e0f1d201',
|
||||
];
|
||||
// const privKeys = privKeyStrings.map(createStacksPrivateKey);
|
||||
|
||||
const pubKeys = privKeyStrings.map(pubKeyfromPrivKey);
|
||||
const pubKeyStrings = pubKeys.map(publicKeyToString);
|
||||
|
||||
const tx = await makeContractCall({
|
||||
const transaction = await makeContractCall({
|
||||
contractAddress,
|
||||
contractName,
|
||||
functionName,
|
||||
@@ -1074,8 +1115,11 @@ test('make a multi-sig contract call', async () => {
|
||||
postConditionMode: PostConditionMode.Allow,
|
||||
anchorMode: AnchorMode.Any,
|
||||
});
|
||||
expect(() => transaction.verifyOrigin()).not.toThrow();
|
||||
|
||||
expect(tx.auth.spendingCondition!.signer).toEqual('04128cacf0764f69b1e291f62d1dcdd8f65be5ab');
|
||||
expect(transaction.auth.spendingCondition!.signer).toEqual(
|
||||
'04128cacf0764f69b1e291f62d1dcdd8f65be5ab'
|
||||
);
|
||||
});
|
||||
|
||||
test('Estimate transaction transfer fee', async () => {
|
||||
|
||||
Reference in New Issue
Block a user