Skip to content
This repository has been archived by the owner on Apr 15, 2021. It is now read-only.

Multi Sig Transactions #109

Merged
merged 24 commits into from Aug 24, 2020
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
79bf8c1
refactor: remove extraneous BufferReader class
reedrosenbluth Aug 11, 2020
d5fb96e
build: package-lock
reedrosenbluth Aug 11, 2020
0d99c47
refactor: hash160 returns buffer instead of string
reedrosenbluth Aug 11, 2020
e3d900a
feat: hashP2SH function for multi-sig addresses
reedrosenbluth Aug 11, 2020
7aed038
fix: public key deserialization
reedrosenbluth Aug 11, 2020
76c2f9a
fix: AddressHashMode comments, and introduce new hash mode types
reedrosenbluth Aug 11, 2020
d01ae8f
refactor: MessageSignature from class to interface
reedrosenbluth Aug 11, 2020
c4dc124
feat: implement MultiSigSpendingCondition, and refactor SpendingCondi…
reedrosenbluth Aug 11, 2020
977a2c6
refactor: rest of lib to work with refactored Auth
reedrosenbluth Aug 11, 2020
db15321
feat: MultiSig transaction signing
reedrosenbluth Aug 11, 2020
3ebe083
test: multisig auth and tx signing
reedrosenbluth Aug 11, 2020
5281ea2
Merge branch 'master' into feat/multi-sig-gen
reedrosenbluth Aug 11, 2020
62e534b
refactor: remove unnecessary version arg and add comment
reedrosenbluth Aug 13, 2020
335230e
feat: build multi-sig token transfer transactions
reedrosenbluth Aug 13, 2020
88bfe0c
test: multi-sig token transfer transaction builder
reedrosenbluth Aug 13, 2020
3e4be15
chore: update changelog
reedrosenbluth Aug 13, 2020
3ee4ed2
fix: make signerKeys optional in tx builder
reedrosenbluth Aug 14, 2020
694b2dc
refactor: separate builder function for signed and unsigned transactions
reedrosenbluth Aug 14, 2020
c6f0eca
fix: typos
reedrosenbluth Aug 19, 2020
909096f
fix: makeSigHashPostSign and clarify MessageSignature type
reedrosenbluth Aug 20, 2020
1833326
fix: revert sponsored AuthType back to standard
reedrosenbluth Aug 24, 2020
1a14863
fix: types import
reedrosenbluth Aug 24, 2020
9899c3c
fix: multi-sig builder test
reedrosenbluth Aug 24, 2020
ff911cf
Merge branch 'master' into feat/multi-sig-gen
reedrosenbluth Aug 24, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,10 @@ All notable changes to the project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
### Added
- Multi-Sig (P2SH) transaction support


## v0.6.0
- Added ability to create sponsored transactions using `sponsorTransaction()`
Expand Down
7 changes: 3 additions & 4 deletions src/authorization.ts
Expand Up @@ -162,9 +162,8 @@ export function createSingleSigSpendingCondition(
nonce: BigNum,
fee: BigNum
): SingleSigSpendingCondition {
const signer = addressFromPublicKeys(AddressVersion.MainnetSingleSig, hashMode, 1, [
createStacksPublicKey(pubKey),
]).hash160;
// address version arg doesn't matter for signer hash generation
const signer = addressFromPublicKeys(0, hashMode, 1, [createStacksPublicKey(pubKey)]).hash160;
const keyEncoding = isCompressed(createStacksPublicKey(pubKey))
? PubKeyEncoding.Compressed
: PubKeyEncoding.Uncompressed;
Expand All @@ -188,7 +187,7 @@ export function createMultiSigSpendingCondition(
): MultiSigSpendingCondition {
const stacksPublicKeys = pubKeys.map(createStacksPublicKey);

// version arg does not matter for signer hash generation
// address version arg doesn't matter for signer hash generation
const signer = addressFromPublicKeys(0, hashMode, numSigs, stacksPublicKeys).hash160;

return {
Expand Down
69 changes: 54 additions & 15 deletions src/builders.ts
Expand Up @@ -9,11 +9,10 @@ import {
} from './payload';

import {
SingleSigSpendingCondition,
StandardAuthorization,
SponsoredAuthorization,
SpendingCondition,
createSingleSigSpendingCondition,
createMultiSigSpendingCondition,
} from './authorization';

import {
Expand All @@ -22,6 +21,7 @@ import {
getPublicKey,
publicKeyToAddress,
pubKeyfromPrivKey,
publicKeyFromBuffer,
} from './keys';

import { TransactionSigner } from './signer';
Expand Down Expand Up @@ -56,6 +56,8 @@ import { fetchPrivate, cvToHex, parseReadOnlyResponse } from './utils';
import * as BigNum from 'bn.js';
import { ClarityValue, PrincipalCV } from './clarity';
import { validateContractCall, ClarityAbi } from './contract-abi';
import { add } from 'lodash';
import { c32address } from 'c32check';

/**
* Lookup the nonce for an address from a core node
Expand Down Expand Up @@ -218,6 +220,12 @@ export async function getAbi(
return JSON.parse(await response.text());
}

export interface MultiSigOptions {
numSignatures: number;
signerKeys: string[];
publicKeys: string[];
}

/**
* STX token transfer transaction options
*
Expand All @@ -236,11 +244,13 @@ export async function getAbi(
* @param {PostCondition[]} postConditions - an array of post conditions to add to the
* transaction
* @param {Boolean} sponsored - true if another account is sponsoring the transaction fees
*
* @param {MultiSigOptions} multiSig - options for a multi-sig transaction
*/
export interface TokenTransferOptions {
recipient: string | PrincipalCV;
amount: BigNum;
senderKey: string;
senderKey?: string;
fee?: BigNum;
nonce?: BigNum;
network?: StacksNetwork;
Expand All @@ -249,6 +259,7 @@ export interface TokenTransferOptions {
postConditionMode?: PostConditionMode;
postConditions?: PostCondition[];
sponsored?: boolean;
multiSig?: MultiSigOptions;
}

/**
Expand Down Expand Up @@ -277,17 +288,29 @@ export async function makeSTXTokenTransfer(

const payload = createTokenTransferPayload(options.recipient, options.amount, options.memo);

const addressHashMode = AddressHashMode.SerializeP2PKH;
const privKey = createStacksPrivateKey(options.senderKey);
const pubKey = getPublicKey(privKey);
let authorization = null;

const spendingCondition = createSingleSigSpendingCondition(
addressHashMode,
publicKeyToString(pubKey),
options.nonce,
options.fee
);
let spendingCondition = null;

if (options.multiSig) {
spendingCondition = createMultiSigSpendingCondition(
AddressHashMode.SerializeP2SH,
options.multiSig.numSignatures,
options.multiSig.publicKeys,
options.nonce,
options.fee
);
} else if (options.senderKey) {
const privKey = createStacksPrivateKey(options.senderKey);
const pubKey = getPublicKey(privKey);
spendingCondition = createSingleSigSpendingCondition(
AddressHashMode.SerializeP2PKH,
publicKeyToString(pubKey),
options.nonce,
options.fee
);
} else {
throw new Error('Transaction options must include either senderKey or multiSig options');
}

if (options.sponsored) {
authorization = new SponsoredAuthorization(spendingCondition);
Expand Down Expand Up @@ -323,14 +346,30 @@ export async function makeSTXTokenTransfer(
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);
}

if (options.senderKey) {
if (options.multiSig) {
const signer = new TransactionSigner(transaction);
let pubKeys = options.multiSig.publicKeys;

for (const key of options.multiSig.signerKeys) {
reedrosenbluth marked this conversation as resolved.
Show resolved Hide resolved
const pubKey = pubKeyfromPrivKey(key);
pubKeys = pubKeys.filter(pk => pk !== pubKey.data.toString('hex'));
signer.signOrigin(createStacksPrivateKey(key));
}

for (const key of pubKeys) {
signer.appendOrigin(publicKeyFromBuffer(Buffer.from(key, 'hex')));
}
} else if (options.senderKey) {
const privKey = createStacksPrivateKey(options.senderKey);
const signer = new TransactionSigner(transaction);
signer.signOrigin(privKey);
} else {
throw new Error('Transaction options must include either senderKey or multiSig options');
}

return transaction;
Expand Down
62 changes: 62 additions & 0 deletions tests/src/builder-tests.ts
Expand Up @@ -46,6 +46,7 @@ import * as BigNum from 'bn.js';

import { enableFetchMocks } from 'jest-fetch-mock';
import { ClarityAbi } from '../../src/contract-abi';
import { createStacksPrivateKey, pubKeyfromPrivKey, publicKeyToString } from '../../src/keys';

enableFetchMocks();

Expand Down Expand Up @@ -187,6 +188,67 @@ test('Make STX token transfer with post conditions', async () => {
expect(serialized).toBe(tx);
});

test('Make Multi-Sig STX token transfer', async () => {
const recipient = standardPrincipalCV('SP3FGQ8Z7JY9BWYZ5WM53E0M9NK7WHJF0691NZ159');
const amount = new BigNum(2500000);
const fee = new BigNum(0);
const nonce = new BigNum(0);
const memo = 'test memo';

const authType = AuthType.Standard;
const addressHashMode = AddressHashMode.SerializeP2SH;

const privKeyStrings = [
'6d430bb91222408e7706c9001cfaeb91b08c2be6d5ac95779ab52c6b431950e001',
'2a584d899fed1d24e26b524f202763c8ab30260167429f157f1c119f550fa6af01',
'd5200dee706ee53ae98a03fba6cf4fdcc5084c30cfa9e1b3462dcdeaa3e0f1d201',
];

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

const transaction = await makeSTXTokenTransfer({
recipient,
amount,
fee,
nonce,
memo: memo,
multiSig: {
numSignatures: 2,
signerKeys: privKeyStrings.slice(0, 2),
publicKeys: pubKeyStrings,
},
});

const serializedTx = transaction.serialize();

const tx =
'00000000010401a23ea89d6529ac48ac766f720e480beec7f1927300000000000000000000000000000000' +
'000000030200dc8061e63a8ed7ca4712c257299b4bdc3938e34ccc01ce979dd74e5483c4f971053a12680c' +
'bfbea87976543a94500314c9a1eaf33986aef97821eb65fb0c60420200c25702efc2bcf5780e17f8ab3fb5' +
'dc509fbfe68c573e844a83b08d6ff0b382c9107690ac4677dc667ca09a7cf9d42e08ff79e6969a80f5ea5a' +
'acbf233ae22d0f0003661ec7479330bf1ef7a4c9d1816f089666a112e72d671048e5424fc528ca51530002' +
'030200000000000516df0ba3e79792be7be5e50a370289accfc8c9e03200000000002625a074657374206d' +
'656d6f00000000000000000000000000000000000000000000000000';

expect(serializedTx.toString('hex')).toBe(tx);

const bufferReader = new BufferReader(serializedTx);
const deserializedTx = deserializeTransaction(bufferReader);

expect(deserializedTx.auth.authType).toBe(authType);

expect(deserializedTx.auth.spendingCondition!.hashMode).toBe(addressHashMode);
expect(deserializedTx.auth.spendingCondition!.nonce.toNumber()).toBe(nonce.toNumber());
expect(deserializedTx.auth.spendingCondition!.fee.toNumber()).toBe(fee.toNumber());
expect(deserializedTx.auth.spendingCondition!.signer).toEqual(
'a23ea89d6529ac48ac766f720e480beec7f19273'
);

const deserializedPayload = deserializedTx.payload as TokenTransferPayload;
expect(deserializedPayload.amount.toNumber()).toBe(amount.toNumber());
});

test('Make smart contract deploy', async () => {
const contractName = 'kv-store';
const codeBody = fs.readFileSync('./tests/src/contracts/kv-store.clar').toString();
Expand Down