Skip to content

Commit a11487c

Browse files
authored
Feature/multisig (#135)
* Add buildMultiSigsTransaction * Fix formt * Fix extra format * Add proxy comment * Make hint optional * Slight fix * Remove BN for it doesnt support decimal * Add multisig example * Fix format * Slight fixes * Remove mixin folder * Fix typo * Slight fixes * Fix path * Remove unuesd packages * Fix test * Export magic * Remove unused file * Update multisig example * Fix format
1 parent a305b81 commit a11487c

File tree

20 files changed

+195
-1511
lines changed

20 files changed

+195
-1511
lines changed

example/multisig.js

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
const { v4 } = require('uuid');
2+
const { MixinApi, buildMultiSigsTransaction, sleep, encodeScript } = require('..');
3+
const keystore = require('../keystore.json');
4+
5+
keystore.user_id = keystore.client_id;
6+
const client = MixinApi({
7+
requestConfig: {
8+
responseCallback: err => {
9+
console.log(err);
10+
},
11+
},
12+
keystore,
13+
});
14+
15+
const readOutput = async (hash, members, threshold, offset = '') => {
16+
let new_offset = offset;
17+
const outputs = await client.multisig.outputs({
18+
members,
19+
threshold,
20+
offset,
21+
limit: 10,
22+
});
23+
24+
// eslint-disable-next-line no-restricted-syntax
25+
for (const output of outputs) {
26+
new_offset = output.updated_at;
27+
if (output.transaction_hash === hash) return output;
28+
}
29+
30+
await sleep(1000 * 5);
31+
return readOutput(hash, members, threshold, new_offset);
32+
};
33+
34+
const main = async () => {
35+
const bot = await client.user.profile();
36+
const asset_id = '965e5c6e-434c-3fa9-b780-c50f43cd955c';
37+
const amount = '0.0001';
38+
const members = [bot.app.creator_id, keystore.user_id];
39+
const threshold = 1;
40+
41+
// 1. send to multisig account
42+
// should have balance in your bot
43+
const sendTxReceipt = await client.transfer.toAddress(keystore.pin, {
44+
asset_id,
45+
amount,
46+
trace_id: v4(),
47+
memo: 'send to multisig',
48+
opponent_multisig: {
49+
threshold,
50+
receivers: members,
51+
},
52+
});
53+
console.log('send to multi-signature account');
54+
console.log('transaction hash:', sendTxReceipt.transaction_hash);
55+
56+
// 2. wait tx complete
57+
console.log('read transaction output...');
58+
const utxo = await readOutput(sendTxReceipt.transaction_hash, members, threshold, '');
59+
console.log(utxo);
60+
61+
// 3. refund
62+
console.log('refund to bot:');
63+
const asset = await client.asset.fetch(asset_id);
64+
const receivers = await client.transfer.outputs([
65+
{
66+
receivers: [keystore.user_id],
67+
index: 0,
68+
},
69+
]);
70+
console.log('generate raw transaction');
71+
const raw = buildMultiSigsTransaction({
72+
version: 2,
73+
asset: asset.mixin_id,
74+
inputs: [
75+
{
76+
hash: utxo.transaction_hash,
77+
index: utxo.output_index,
78+
},
79+
],
80+
outputs: [
81+
{
82+
amount,
83+
mask: receivers[0].mask,
84+
keys: receivers[0].keys,
85+
script: encodeScript(threshold),
86+
},
87+
],
88+
extra: 'refund',
89+
});
90+
91+
// Generate a multi-signature
92+
console.log('generate a multi-signature request...');
93+
const multisig = await client.multisig.create('sign', raw);
94+
95+
// Sign a multi-signature
96+
console.log('sign...');
97+
const signed = await client.multisig.sign(keystore.pin, multisig.request_id);
98+
console.log(signed);
99+
100+
// Send signed tx to mainnet
101+
console.log('send to mainnet...');
102+
const res = await client.external.proxy({
103+
method: 'sendrawtransaction',
104+
params: [signed.raw_transaction],
105+
});
106+
console.log(res);
107+
};
108+
109+
main();

package.json

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,6 @@
8181
"@ethersproject/abi": "^5.6.3",
8282
"@ethersproject/providers": "^5.6.8",
8383
"@types/axios": "^0.14.0",
84-
"@types/bn.js": "^5.1.0",
85-
"@types/jsonwebtoken": "^8.5.8",
8684
"@types/node-forge": "^1.0.2",
8785
"@types/serialize-javascript": "^5.0.2",
8886
"@types/uuid": "^8.3.4",

src/client/external.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@ export const ExternalKeystoreClient = (axiosInstance: AxiosInstance) => ({
2020
*/
2121
exchangeRates: (): Promise<ExchangeRateResponse[]> => axiosInstance.get<unknown, ExchangeRateResponse[]>('/external/fiats'),
2222

23+
/**
24+
* Submit a raw transaction to a random mainnet node
25+
* {
26+
* method: 'sendrawtransaction',
27+
* params: array of transaction hash
28+
* }
29+
* */
2330
proxy: (params: ProxyRequest): Promise<any> => axiosInstance.post<unknown, any>('/external/proxy', params),
2431
});
2532

src/client/message.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,7 @@ import {
1919
TransferMessageRequest,
2020
RecallMessageRequest,
2121
} from './types/message';
22-
import { base64url } from '../mixin/sign';
23-
import { uniqueConversationID } from './utils/uniq';
24-
import { buildClient } from './utils/client';
22+
import { uniqueConversationID, base64RawURLEncode, buildClient } from './utils';
2523

2624
/**
2725
* Methods to send messages
@@ -41,7 +39,7 @@ export const MessageKeystoreClient = (axiosInstance: AxiosInstance, keystore: Ke
4139
recipient_id: recipientID,
4240
conversation_id: uniqueConversationID(keystore!.user_id, recipientID),
4341
message_id: uuid(),
44-
data: base64url(Buffer.from(data)),
42+
data: base64RawURLEncode(Buffer.from(data)),
4543
};
4644
await send(messageRequest);
4745
return messageRequest;

src/client/types/multisig.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { Input, Output } from '../../mvm';
2+
13
export type UTXOState = 'unspent' | 'signed' | 'spent';
24

35
export type MultisigInitAction = 'sign' | 'unlock';
@@ -55,3 +57,13 @@ export interface MultisigRequestResponse {
5557
updated_at: string;
5658
code_id: string;
5759
}
60+
61+
export interface MultisigTransaction {
62+
/** 2 */
63+
version: number;
64+
/** mixin_id of asset */
65+
asset: string;
66+
inputs: Input[];
67+
outputs: Output[];
68+
extra: string;
69+
}

src/client/types/transaction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface RawTransactionRequest {
1616
export interface GhostInputRequest {
1717
receivers: string[];
1818
index: number;
19-
hint: string;
19+
hint?: string;
2020
}
2121

2222
export interface GhostKeysResponse {

src/client/utils/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from './auth';
22
export * from './base64';
33
export * from './client';
4+
export * from './multisigs';
45
export * from './nfo';
56
export * from './pin';
67
export * from './sleep';

src/client/utils/multisigs.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { MultisigTransaction } from '../types';
2+
import { Encoder, magic } from '../../mvm';
3+
4+
export const TxVersion = 0x02;
5+
6+
export const encodeScript = (threshold: number) => {
7+
let s = threshold.toString(16);
8+
if (s.length === 1) s = `0${s}`;
9+
if (s.length > 2) throw new Error(`INVALID THRESHOLD ${threshold}`);
10+
11+
return `fffe${s}`;
12+
};
13+
14+
export const encodeTx = (tx: MultisigTransaction) => {
15+
const enc = new Encoder(Buffer.from([]));
16+
17+
enc.write(magic);
18+
enc.write(Buffer.from([0x00, tx.version]));
19+
enc.write(Buffer.from(tx.asset, 'hex'));
20+
21+
enc.writeInt(tx.inputs.length);
22+
tx.inputs.forEach(input => {
23+
enc.encodeInput(input);
24+
});
25+
26+
enc.writeInt(tx.outputs.length);
27+
tx.outputs.forEach(output => {
28+
enc.encodeOutput(output);
29+
});
30+
31+
const extra = Buffer.from(tx.extra);
32+
enc.writeInt(extra.byteLength);
33+
enc.write(extra);
34+
35+
enc.writeInt(0);
36+
enc.write(Buffer.from([]));
37+
38+
return enc.buf.toString('hex');
39+
};
40+
41+
/**
42+
* Generate raw for multi-signature transaction.
43+
* The total amount of input utxos should be equal to the total amount of output utxos.
44+
* */
45+
export const buildMultiSigsTransaction = (transaction: MultisigTransaction) => {
46+
if (transaction.version !== TxVersion) throw new Error('Invalid Version!');
47+
48+
const tx = {
49+
...transaction,
50+
outputs: transaction.outputs.filter(output => !!output.mask),
51+
extra: Buffer.from(transaction.extra).toString('hex'),
52+
};
53+
return encodeTx(tx);
54+
};

src/client/utils/nfo.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { parse as UUIDParse, stringify } from 'uuid';
22
import { NFOMemo } from '../types';
3-
import { newHash } from './uniq';
43
import { base64RawURLEncode } from './base64';
4+
import { newHash } from './uniq';
55
import { Encoder, Decoder } from '../../mvm';
66

77
const Prefix = 'NFO';

src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export * from './client';
22
export * from './mvm';
33
export * from './webview';
4-
export * from './mixin/dump_transacion';
54
export * from './constant';

0 commit comments

Comments
 (0)