Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/multisig support contract deploy fork #1544

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/cli/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
ClarityAbi,
ClarityValue,
ContractCallPayload,
ContractDeployOptions,
SignedContractDeployOptions,
createStacksPrivateKey,
cvToString,
estimateContractDeploy,
Expand Down Expand Up @@ -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,
Expand Down
92 changes: 65 additions & 27 deletions packages/transactions/src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import { ClarityAbi, validateContractCall } from './contract-abi';
import { NoEstimateAvailableError } from './errors';
import {
createStacksPrivateKey,
createStacksPublicKey,
getPublicKey,
pubKeyfromPrivKey,
publicKeyFromBytes,
Expand Down Expand Up @@ -707,16 +706,26 @@ 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;
}

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.
*
Expand Down Expand Up @@ -772,31 +781,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),
Expand All @@ -815,17 +842,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);
Expand Down Expand Up @@ -863,7 +901,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);
}
Expand Down
30 changes: 30 additions & 0 deletions packages/transactions/tests/builder.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -807,6 +807,36 @@ 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 privKeys = privKeyStrings.map(createStacksPrivateKey);

const pubKeys = privKeyStrings.map(pubKeyfromPrivKey);
const pubKeyStrings = pubKeys.map(publicKeyToString);

const tx = await makeContractDeploy({
codeBody,
contractName,
publicKeys: pubKeyStrings,
numSignatures: 3,
signerKeys: privKeyStrings,
fee,
nonce,
network: new StacksTestnet(),
anchorMode: AnchorMode.Any,
});

expect(tx.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();
Expand Down
Loading