Skip to content

Commit

Permalink
Merge pull request #1782 from demergent-labs/psbt
Browse files Browse the repository at this point in the history
Add a Psbt example
  • Loading branch information
lastmjs authored Jun 4, 2024
2 parents 0162634 + dcdd6d0 commit bda1028
Show file tree
Hide file tree
Showing 50 changed files with 4,078 additions and 79 deletions.
5 changes: 3 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ jobs:
"examples/autoreload",
"examples/basic_bitcoin",
"examples/bitcoin",
"examples/bitcoinjs-lib",
"examples/bitcore-lib",
"examples/bitcoin_psbt",
"examples/bitcoinjs_lib",
"examples/bitcore_lib",
"examples/blob_array",
"examples/bytes",
"examples/call_raw",
Expand Down
2 changes: 1 addition & 1 deletion examples/autoreload/test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ export function getTests(canisterId: string): Test[] {
},
{
name: 'waiting for Azle to reload',
wait: 30_000
wait: 60_000
},
{
name: '/test',
Expand Down
4 changes: 2 additions & 2 deletions examples/basic_bitcoin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fi
dfx start --clean --host 127.0.0.1:8000
```

## basic_bitcion
## basic_bitcoin

```bash
BITCOIN_NETWORK=regtest dfx deploy'
Expand All @@ -57,7 +57,7 @@ You can now use the `send` function to try sending some BTC to another address,
.bitcoin/bin/bitcoin-cli -conf=$(pwd)/.bitcoin.conf generatetoaddress 1 <your-canister-btc-address>
```
You should see some output such as `2023-05-30T20:33:25Z CreateNewBlock(): block weight: 1804 txs: 1 fees: 454 sigops 408` in your Bitcoin node's terminal indicating that your transaction was included in the block.
You should see some output such as `2023-05-30T20:33:25Z CreateNewBlock(): block weight: 1804 txs: 1 fees: 454 sigops 408` in your Bitcoin node's terminal (not the dfx terminal) indicating that your transaction was included in the block.

Now if you call the functions with the `n2dcQfuwFw7M2UYzLfM6P7DwewsQaygb8S` address you should see a new balance, utxos, and fee percentiles.

Expand Down
1 change: 1 addition & 0 deletions examples/basic_bitcoin/dfx.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
},
"defaults": {
"bitcoin": {
"canister_init_arg": "(record { stability_threshold = 1000 : nat; network = variant { regtest }; blocks_source = principal \"aaaaa-aa\"; fees = record { get_utxos_base = 0 : nat; get_utxos_cycles_per_ten_instructions = 0 : nat; get_utxos_maximum = 0 : nat; get_balance = 0 : nat; get_balance_maximum = 0 : nat; get_current_fee_percentiles = 0 : nat; get_current_fee_percentiles_maximum = 0 : nat; send_transaction_base = 0 : nat; send_transaction_per_byte = 0 : nat; }; syncing = variant { enabled }; api_access = variant { enabled }; disable_api_if_not_fully_synced = variant { enabled }})",
"enabled": true,
"nodes": ["127.0.0.1:18444"],
"log_level": "info"
Expand Down
1 change: 1 addition & 0 deletions examples/basic_bitcoin/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions examples/basic_bitcoin/package.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"name": "basic_bitcoin",
"scripts": {
"install": "./scripts/install.sh",
"pretest": "ts-node --transpile-only --ignore=false test/pretest.ts",
Expand Down
4 changes: 2 additions & 2 deletions examples/basic_bitcoin/src/bitcoin_wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { Buffer } from 'buffer';
import * as bitcoinApi from './bitcoin_api';
import * as ecdsaApi from './ecdsa_api';

type SignFun = (
export type SignFun = (
keyName: string,
derivationPath: Uint8Array[],
messageHash: Uint8Array
Expand Down Expand Up @@ -289,7 +289,7 @@ function publicKeyToP2pkhAddress(
}

// A mock for rubber-stamping ECDSA signatures.
function mockSigner(
export function mockSigner(
_keyName: string,
_derivationPath: Uint8Array[],
_messageHash: Uint8Array
Expand Down
2 changes: 1 addition & 1 deletion examples/basic_bitcoin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ app.post('/send', async (req, res) => {

app.listen();

function determineKeyName(network: BitcoinNetwork): string {
export function determineKeyName(network: BitcoinNetwork): string {
if (network.mainnet === null) {
return 'test_key_1';
} else if (network.testnet === null) {
Expand Down
38 changes: 38 additions & 0 deletions examples/basic_bitcoin/test/bitcoin_daemon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import { existsSync, rmSync } from 'fs-extra';

export async function whileRunningBitcoinDaemon(
callback: () => Promise<boolean> | void
) {
const bitcoinDaemon = await startBitcoinDaemon();
await callback();
bitcoinDaemon.kill();
}

async function startBitcoinDaemon(): Promise<ChildProcessWithoutNullStreams> {
if (existsSync(`.bitcoin/data/regtest`)) {
rmSync('.bitcoin/data/regtest', { recursive: true, force: true });
}
const bitcoinDaemon = spawn('.bitcoin/bin/bitcoind', [
`-conf=${process.cwd()}/.bitcoin.conf`,
`-datadir=${process.cwd()}/.bitcoin/data`,
'--port=18444'
]);

process.on('uncaughtException', () => {
if (!bitcoinDaemon.killed) {
bitcoinDaemon.kill();
}
});

process.on('exit', () => {
if (!bitcoinDaemon.killed) {
bitcoinDaemon.kill();
}
});

console.info(`starting bitcoind...`);
// This await is necessary to ensure the daemon is running
await new Promise((resolve) => setTimeout(resolve, 5_000));
return bitcoinDaemon;
}
45 changes: 5 additions & 40 deletions examples/basic_bitcoin/test/test.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,11 @@
import { getCanisterId } from 'azle/dfx';
import { runTests } from 'azle/test';
import { ChildProcessWithoutNullStreams, spawn } from 'child_process';
import { existsSync, rmSync } from 'fs-extra';

import { getTests } from './tests';
import { whileRunningBitcoinDaemon } from './bitcoin_daemon';
import { getP2pkhAddress, getTests, P2PKH_ADDRESS_FORM } from './tests';

const canisterId = getCanisterId('basic_bitcoin');

export async function whileRunningBitcoinDaemon(
callback: () => Promise<boolean> | void
) {
const bitcoinDaemon = await startBitcoinDaemon();
await callback();
bitcoinDaemon.kill();
}

async function startBitcoinDaemon(): Promise<ChildProcessWithoutNullStreams> {
if (existsSync(`.bitcoin/data/regtest`)) {
rmSync('.bitcoin/data/regtest', { recursive: true, force: true });
}
const bitcoinDaemon = spawn('.bitcoin/bin/bitcoind', [
`-conf=${process.cwd()}/.bitcoin.conf`,
`-datadir=${process.cwd()}/.bitcoin/data`,
'--port=18444'
]);

process.on('uncaughtException', () => {
if (!bitcoinDaemon.killed) {
bitcoinDaemon.kill();
}
});

process.on('exit', () => {
if (!bitcoinDaemon.killed) {
bitcoinDaemon.kill();
}
});

console.info(`starting bitcoind...`);
// This await is necessary to ensure the daemon is running
await new Promise((resolve) => setTimeout(resolve, 5000));
return bitcoinDaemon;
}

whileRunningBitcoinDaemon(() => runTests(getTests(canisterId)));
whileRunningBitcoinDaemon(() =>
runTests(getTests(canisterId, getP2pkhAddress, P2PKH_ADDRESS_FORM))
);
48 changes: 30 additions & 18 deletions examples/basic_bitcoin/test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getTransaction
} from './bitcoin';

export const P2PKH_ADDRESS_FORM = 'mhVmPSYFraAYnA4ZP6KUx41P3dKgAg27Cm'; // p2pkh-address on the regtest will generally be of this form, starting with m or n and this many characters.
const SINGLE_BLOCK_REWARD = 5_000_000_000n;
const FIRST_MINING_SESSION = 101;
const FIRST_AMOUNT_SENT = SINGLE_BLOCK_REWARD / 2n;
Expand All @@ -24,9 +25,14 @@ let lastTxid = '';
let toAddressPreviousBalance = 0n;
let canisterPreviousBalance = 0n;

export function getTests(canisterId: string): Test[] {
export type AddressFunc = (origin: string) => Promise<string>;

export function getTests(
canisterId: string,
getAddress: AddressFunc,
addressForm: string
): Test[] {
const origin = `http://${canisterId}.localhost:8000`;
const canisterAddressForm = 'mhVmPSYFraAYnA4ZP6KUx41P3dKgAg27Cm'; // p2pkh-address on the regtest will generally be of this form, starting with m or n and this many characters.
return [
{
name: 'Set up minting wallet',
Expand All @@ -35,17 +41,17 @@ export function getTests(canisterId: string): Test[] {
}
},
{
name: '/get-p2pkh-address',
name: '/get-address',
test: async () => {
const address = await getP2pkhAddress(origin);
const address = await getAddress(origin);

return { Ok: canisterAddressForm.length === address.length };
return { Ok: addressForm.length === address.length };
}
},
{
name: '/get-balance',
test: async () => {
const address = await getP2pkhAddress(origin);
const address = await getAddress(origin);
const balance = await getBalance(origin, address);

return compareBalances(0n, balance);
Expand All @@ -54,15 +60,15 @@ export function getTests(canisterId: string): Test[] {
{
name: 'first mint BTC',
prep: async () => {
const address = await getP2pkhAddress(origin);
const address = await getAddress(origin);
generateToAddress(address, FIRST_MINING_SESSION);
}
},
{ name: 'wait for blocks to settle', wait: 30_000 },
{
name: '/get-balance',
test: async () => {
const address = await getP2pkhAddress(origin);
const address = await getAddress(origin);
const balance = await getBalance(origin, address);

return compareBalances(
Expand All @@ -74,7 +80,7 @@ export function getTests(canisterId: string): Test[] {
{
name: '/get-utxos',
test: async () => {
const address = await getP2pkhAddress(origin);
const address = await getAddress(origin);

const response = await fetch(
`${origin}/get-utxos?address=${address}`,
Expand Down Expand Up @@ -153,7 +159,7 @@ export function getTests(canisterId: string): Test[] {
{
name: '/get-balance final',
test: async () => {
const address = await getP2pkhAddress(origin);
const address = await getAddress(origin);
const balance = await getBalance(origin, address);
canisterPreviousBalance = balance;

Expand Down Expand Up @@ -233,7 +239,7 @@ export function getTests(canisterId: string): Test[] {
{
name: '/get-balance big',
test: async () => {
const address = await getP2pkhAddress(origin);
const address = await getAddress(origin);
const balance = await getBalance(origin, address);

// At the time this transaction was made, the next utxos to use will be from block rewards.
Expand Down Expand Up @@ -282,14 +288,14 @@ export function getTests(canisterId: string): Test[] {
const feePercentiles = jsonParse(await response.text());

return {
Ok: feePercentiles.length === 0
Ok: feePercentiles.length === 101
};
}
}
];
}

function compareBalances(
export function compareBalances(
expected: bigint,
actual: bigint
): AzleResult<boolean, string> {
Expand All @@ -308,7 +314,10 @@ function compareBalances(
* @param totalInputValue
* @returns
*/
function getFeeFromTransaction(txid: string, totalInputValue: bigint): bigint {
export function getFeeFromTransaction(
txid: string,
totalInputValue: bigint
): bigint {
const previousTransaction = getTransaction(txid);
const outputValue = BigInt(getTotalOutput(previousTransaction));
return totalInputValue - outputValue;
Expand All @@ -320,27 +329,30 @@ function getTotalOutput(tx: Transaction): number {
}, 0);
}

async function getP2pkhAddress(origin: string): Promise<string> {
export async function getP2pkhAddress(origin: string): Promise<string> {
const response = await fetch(`${origin}/get-p2pkh-address`, {
headers: [['X-Ic-Force-Update', 'true']]
});
return await response.text();
}

async function getBalance(origin: string, address: string): Promise<bigint> {
export async function getBalance(
origin: string,
address: string
): Promise<bigint> {
const response = await fetch(`${origin}/get-balance?address=${address}`, {
headers: [['X-Ic-Force-Update', 'true']]
});
return jsonParse(await response.text());
}

function checkUtxos(utxos: Utxo[]): boolean {
export function checkUtxos(utxos: Utxo[]): boolean {
return utxos.every(
(utxo) => utxo.value === SINGLE_BLOCK_REWARD && utxo.outpoint.vout === 0
);
}

async function waitForMempool() {
export async function waitForMempool() {
for (let i = 0; i < 60; i++) {
if (getMempoolCount() > 0) {
console.info('done waiting');
Expand Down
4 changes: 2 additions & 2 deletions examples/basic_bitcoin/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"target": "ES2020",
"moduleResolution": "node",
"allowJs": true,
"allowSyntheticDefaultImports": true,
"outDir": "HACK_BECAUSE_OF_ALLOW_JS"
"outDir": "HACK_BECAUSE_OF_ALLOW_JS",
"allowSyntheticDefaultImports": true
}
}
10 changes: 10 additions & 0 deletions examples/bitcoin_psbt/.bitcoin.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Enable regtest mode. This is required to setup a private bitcoin network.
regtest=1

# Dummy credentials that are required by `bitcoin-cli`.
rpcuser=ic-btc-integration
rpcpassword=QPQiNaph19FqUsCrBRN0FII7lyM26B51fAMeBQzCb-E=
rpcauth=ic-btc-integration:cdf2741387f3a12438f69092f0fdad8e$62081498c98bee09a0dce2b30671123fa561932992ce377585e8e08bb0c11dfa

# Enable indexing so we can look up transactions by their ids
txindex=1
5 changes: 5 additions & 0 deletions examples/bitcoin_psbt/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.azle
.bitcoin
.dfx
dfx_generated
node_modules
Loading

0 comments on commit bda1028

Please sign in to comment.