-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathcli.js
More file actions
executable file
·278 lines (207 loc) · 9.38 KB
/
cli.js
File metadata and controls
executable file
·278 lines (207 loc) · 9.38 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
#!/usr/bin/env node
const usage = `
protocol-js get 0d45da0f1eabeba2b383a09133f82d8b9fb0c7cbbd9d8ede626c45718df6660f
Get a transaction by hash from WhatsOnChain and decode it
protocol-js transactions 1DJWCvgTFQBxYiDnVX3edG1A9kEidzLs9a
Get all transactions for an address from WhatsOnChain and decode them
protocol-js key private.key m/1/2
Make a private key if it does not exist and print the address of a BIP-32 derivation the key
protocol-js transfer private.key m/1 m/2 1 1DJWCvgTFQBxYiDnVX3edG1A9kEidzLs9a
protocol-js transfer <private key file> <bsv path> <token path> <token quantity> <target address>
Transfer tokens from one address using bsv funding from another to a target address
protocol-js fees 78934b50a28b465319cdc61fbe960d6b5c69c9683cc35c13d1558f3014581276
Re-compute the fees (settlement, contract and boomerang) for a broadcast transaction
`;
import { Input, Output, Tx, base58AddressToProtocolAddress } from "@tokenized/protocol-js";
import fetch from "node-fetch";
import { publicKeyToAddress } from "./crypto/address.js";
import Hash from "./crypto/Hash.js";
import { bytesToHex } from "./crypto/utils.js";
import { loadKey } from "./keys.js";
import { broadcastTransaction, getAddressHistory, getTransaction } from "./network.js";
import { getHashes } from "crypto";
import { protocolAddressToBase58 } from "./crypto/Output.js";
import { computeTransferFees } from "./protocol.js";
if (!getHashes().includes("ripemd160")) {
console.log("openssl 3 does not provide ripemd160 required for bitcoin hashing");
console.log("Suggest using Node 16 or lower, or export NODE_OPTIONS=--openssl-legacy-provider");
process.exit(1);
}
const { round } = Math;
const feeRate = 0.05;
const smartContractFeeRate = 0.05;
const CONTRACT_OPERATOR_SERVICE_TYPE = 3;
async function get(txid) {
let transaction = await getTransaction(txid);
console.log("%s", new Tx(transaction));
}
async function key(privateKeyFile, path = "m") {
let derivedKey = await loadKey(privateKeyFile, path);
console.log(publicKeyToAddress(derivedKey.publicKey().toBytes()));
}
async function transactions(address) {
for (let tx of await getAddressTransactions(address)) {
console.log(`ID: ${tx.hash} ; height: ${tx.height}`);
console.log("%s", tx);
console.log("".padStart(process.stdout.columns, "-"));
}
}
function annotateSpentOutputs(txs, address) {
for (let tx of txs) {
for (let [index, output] of tx.outputs.map((output, index) => [index, output])) {
if (output.payload?.p2pkh == address) {
output.spent = !!txs.find(({ inputs }) => inputs.find(input => input.hash == tx.hash && input.index == index));
}
}
}
return txs;
}
async function getAddressTransactions(address) {
let txs = [];
for (let { tx_hash: hash, height } of await getAddressHistory(address)) {
let tx = new Tx(await getTransaction(hash));
tx.hash = hash;
tx.height = height;
txs.push(tx);
}
annotateSpentOutputs(txs, address);
return txs;
}
async function getOutput(address, filter) {
for (let tx of await getAddressTransactions(address)) {
let hash = tx.hash;
let index = tx.outputs.findIndex(filter);
if (index >= 0) {
let output = tx.outputs[index];
return { hash, index, tx, output };
}
}
}
async function getAction(address, code) {
return await getOutput(address, ({ payload }) => payload?.actionCode == code);
}
async function getBSV(address, minValue) {
return await getOutput(address, ({ payload, value, spent }) => !spent && payload?.p2pkh == address && value >= minValue);
}
// 1P4FaWQofBNhaR1bVPYxRg31N8zw5dkTtq is the Tokenized smart contract operator address
async function create(contractOperatorAddress) {
const operatorContract = (await getAction(contractOperatorAddress, "C2"));
if (!operatorContract) {
throw new Error("Operator contract not found");
}
const { output: { payload: { message: { Services } } } } = operatorContract;
const url = Services.find(({ Type }) => Type == CONTRACT_OPERATOR_SERVICE_TYPE).URL;
console.log((await (await fetch(new URL('/new_contract', url)))).json());
}
async function transfer(privateKeyFile, bsvPath, tokenPath, quantity, targetAddress) {
if (!targetAddress) {
console.log(usage);
return 1;
}
const bsvXKey = await loadKey(privateKeyFile, bsvPath);
const tokenXKey = await loadKey(privateKeyFile, tokenPath);
let tx = new Tx();
let tokenAddress = publicKeyToAddress(tokenXKey.publicKey().toBytes());
let tokens = await getAction(tokenAddress, "T2");
if (!tokens) {
throw new Error("Tokens not found at address");
}
let contractAddresses = tokens.tx.inputs.map(input => input.payload.p2pkh);
if (contractAddresses.length > 1) {
throw new Error("Unsure which token to send");
}
let [contractAddress] = contractAddresses;
let instrument = tokens.output.payload.message.Instruments[0];
let contract = await getAction(contractAddress, "C2");
let contractFee = contract.output.payload.message.ContractFee.toNumber();
let transfer = {
Instruments: [
{
InstrumentType: instrument.InstrumentType,
InstrumentCode: instrument.InstrumentCode,
InstrumentSenders: [{ Quantity: quantity, Index: 0 }],
InstrumentReceivers: [{
Address: base58AddressToProtocolAddress(targetAddress),
Quantity: quantity
}]
}
]
};
let [settlementFee, boomerangFee] = computeTransferFees(transfer, smartContractFeeRate);
if (boomerangFee > 0) {
throw new Error("Unexpected boomerang");
}
const contractAgentFee = BigInt(contractFee + settlementFee);
const maximumMinerFee = 500n;
let bsvAddress = publicKeyToAddress(bsvXKey.publicKey().toBytes());
let requiredValue = contractAgentFee + maximumMinerFee;
let bsv = await getBSV(bsvAddress, requiredValue);
if (!bsv) {
throw new Error(`Insufficient funds, required: ${requiredValue}`);
}
let tokenUtxo = await getBSV(tokenAddress, 0);
if (!tokenUtxo < 0) {
throw new Error("Unable to find token utxo");
}
tx.inputs.push(Input.p2pkh(tokenUtxo, tokenXKey.key()));
tx.inputs.push(Input.p2pkh(bsv, bsvXKey.key()));
tx.outputs.push(Output.p2pkh(contractAddress, contractAgentFee));
tx.outputs.push(Output.tokenized("T1", transfer));
let changeOutput = Output.p2pkh(bsvAddress, 0);
tx.outputs.push(changeOutput);
let fee = BigInt(round(feeRate * tx.size()));
changeOutput.value = bsv.output.value - contractAgentFee - fee;
for (let [{ spendingOutput, key }, index] of tx.inputs.map((i, index) => [i, index])) {
await tx.signP2PKHInput(key, index, spendingOutput.script, spendingOutput.value);
}
console.log("%s", tx);
console.log(bytesToHex(tx.toBytes()));
console.log("Transaction ID:", await broadcastTransaction("main", bytesToHex(tx.toBytes())));
}
// currently only sends bsv:
async function send(privateKeyFile, path, inputs, quantity, targetAddress, changeAddress) {
const key = (await loadKey(privateKeyFile, path)).key();
let tx = new Tx();
let inputValue = 0;
let spendingOutputs = await Promise.all(
inputs.split(",")
.map(i => i.split(":"))
.map(async ([txId, number]) => {
let spendingOutput = new Tx(await getTransaction(txId)).outputs[number];
tx.inputs.push(new Input(new Hash(txId), number, new Uint8Array(71 + 32)));
inputValue += Number(spendingOutput.value);
return spendingOutput;
})
);
tx.outputs.push(Output.p2pkh(targetAddress, quantity));
let changeOutput = Output.p2pkh(changeAddress, 0);
tx.outputs.push(changeOutput);
let fee = round(feeRate * tx.size());
changeOutput.value = inputValue - quantity - fee;
for (let [spendingOutput, index] of spendingOutputs.map((output, index) => [output, index])) {
await tx.signP2PKHInput(key, index, spendingOutput.script, spendingOutput.value);
}
console.log("%s", tx);
console.log(bytesToHex(tx.toBytes()));
await broadcastTransaction("main", bytesToHex(tx.toBytes()));
}
async function fees(txid) {
let tx = new Tx(await getTransaction(txid));
let transfer = tx.outputs.find(({ payload }) => payload?.actionCode == "T1").payload.message;
let contractAddresses = transfer.Instruments.map(({ ContractIndex }) => tx.outputs[ContractIndex].payload.p2pkh);
let contracts = await Promise.all(contractAddresses.map(async address => (await getAction(address, "C2")).output.payload.message));
let contractFees = contracts.map(contract => contract.ContractFee.toNumber());
let [settlementFee, boomerangFee] = computeTransferFees(transfer, 0.05);
console.log("Contract addresses:", contractAddresses);
console.log("Settlement fee:", settlementFee);
console.log("Boomerang fee:", boomerangFee);
console.log("Contract fees:", contractFees);
}
const commands = { get, key, send, transactions, create, transfer, fees };
function help() {
console.log(usage);
}
async function main(commandName, ...args) {
await (commands[commandName] || help)(...args);
}
await main(...process.argv.slice(2)).catch(console.error).then((code = 1) => process.exitCode = code);