Skip to content

Commit be57a00

Browse files
Merge pull request #5654 from BitGo/BTC-0-omni-example
docs(root): create example for interacting with omni
2 parents 5aeae54 + 372a20c commit be57a00

File tree

3 files changed

+204
-0
lines changed

3 files changed

+204
-0
lines changed

examples/ts/btc/omni/config.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { BitGoAPI } from '@bitgo/sdk-api';
2+
import { Btc, Tbtc4 } from '@bitgo/sdk-coin-btc';
3+
import * as utxolib from '@bitgo/utxo-lib';
4+
5+
const env = 'test' as 'test' | 'prod';
6+
7+
const accessToken = '';
8+
const walletId = '';
9+
const walletPassphrase = '';
10+
// optional
11+
const otp = '';
12+
13+
const sdk = new BitGoAPI({ env });
14+
sdk.register('tbtc4', Tbtc4.createInstance);
15+
sdk.register('btc', Btc.createInstance);
16+
sdk.authenticateWithAccessToken({ accessToken });
17+
18+
export const omniConfig = {
19+
env,
20+
coin: env === 'test' ? 'tbtc4' : 'btc',
21+
network: env === 'test' ? utxolib.networks.bitcoinTestnet4 : utxolib.networks.bitcoin,
22+
sdk,
23+
walletPassphrase,
24+
walletId,
25+
otp,
26+
MEMPOOL_PREFIX: env === 'test' ? 'testnet4/' : '',
27+
OMNI_PREFIX: Buffer.from('6f6d6e69', 'hex'),
28+
};

examples/ts/btc/omni/debugging.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { Wallet } from '@bitgo/sdk-core';
2+
import * as superagent from 'superagent';
3+
import * as utxolib from '@bitgo/utxo-lib';
4+
import { omniConfig } from './config';
5+
6+
export async function mintOmniAsset(wallet: Wallet, address: string, feeRate = 20_000): Promise<void> {
7+
const transactionVersion = Buffer.alloc(2);
8+
const transactionType = Buffer.alloc(2);
9+
transactionType.writeUint16BE(50);
10+
const ecoSystem = Buffer.alloc(1);
11+
ecoSystem.writeInt8(2);
12+
const propertyType = Buffer.alloc(2);
13+
propertyType.writeUint16BE(2);
14+
const previousPropertyID = Buffer.alloc(4);
15+
16+
const category = Buffer.from('Other\0');
17+
const subCategory = Buffer.from('Other\0');
18+
const propertyTitle = Buffer.from('Testcoin\0');
19+
const propertyURL = Buffer.from('https://example.com\0');
20+
const propertyData = Buffer.from('\0');
21+
22+
const amount = Buffer.alloc(8);
23+
amount.writeBigUint64BE(BigInt(100000 * 10 ** 8));
24+
25+
const res = await superagent.get(`https://mempool.space/${omniConfig.MEMPOOL_PREFIX}api/address/${address}/utxo`);
26+
const unspent = res.body[0];
27+
const unspent_id = unspent.txid + ':' + unspent.vout;
28+
29+
const omniScript = Buffer.concat([
30+
omniConfig.OMNI_PREFIX, // omni
31+
transactionVersion,
32+
transactionType,
33+
ecoSystem,
34+
propertyType,
35+
previousPropertyID,
36+
category,
37+
subCategory,
38+
propertyTitle,
39+
propertyURL,
40+
propertyData,
41+
amount,
42+
]);
43+
44+
const output = utxolib.payments.embed({ data: [omniScript], network: utxolib.networks.bitcoin }).output;
45+
if (!output) {
46+
throw new Error('Invalid output');
47+
}
48+
const script = output.toString('hex');
49+
const tx = await wallet.sendMany({
50+
recipients: [
51+
{
52+
amount: '0',
53+
address: `scriptPubkey:${script}`,
54+
},
55+
],
56+
isReplaceableByFee: true,
57+
feeRate,
58+
walletPassphrase: omniConfig.walletPassphrase,
59+
changeAddress: address,
60+
unspents: [unspent_id],
61+
});
62+
console.log('Omni asset created: ', tx);
63+
}

examples/ts/btc/omni/index.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
/**
2+
* Send or create an omni asset from a multi-sig wallet at BitGo.
3+
*
4+
* Copyright 2024, BitGo, Inc. All Rights Reserved.
5+
*/
6+
import { Wallet } from 'modules/bitgo/src';
7+
import { omniConfig } from './config';
8+
import * as superagent from 'superagent';
9+
import * as utxolib from '@bitgo/utxo-lib';
10+
11+
const RECEIVE_ADDRESS = '';
12+
const SEND_ADDRESS = '';
13+
const ASSET_ID = 31;
14+
const BASE_AMOUNT = 729100000n; // this is currently 7.291 USDT
15+
16+
async function getWallet() {
17+
return await omniConfig.sdk.coin(omniConfig.coin).wallets().get({ id: omniConfig.walletId });
18+
}
19+
20+
/**
21+
* Send an omni asset to a receiver. This function is used when you have sent an omni asset to a BitGo BTC wallet
22+
* and need to manually recover it
23+
* This function assumes that:
24+
* - Your address has at least one unspent that is large enough to cover the transaction
25+
* - The receiver address is a legacy or wrapped segwit ([reference](https://developers.bitgo.com/coins/address-types)) address, otherwise the transaction will not be recognized by the omni explorer.
26+
* @param wallet - The wallet to send the omni asset from.
27+
* @param receiver - The address to send the omni asset to (legacy address required).
28+
* @param sender - The address to send the omni asset from (legacy address required).
29+
* @param omniBaseAmount - The amount of the omni asset to send
30+
* with respect to its smallest unit (e.g., microcents for USDT).
31+
* Can be found at https://api.omniexplorer.info/v1/transaction/tx/{prev_txid}
32+
* by multiplying `amount` by 10e8 if `divisible` is true.
33+
* If `divisible` is false, `amount` is the amount of the omni asset to send
34+
* @param assetId - The id of the omni asset to send.
35+
* Can be found at https://api.omniexplorer.info/v1/transaction/tx/{prev_txid}
36+
* by looking at the `propertyid` field.
37+
* This is 31 for USDT.
38+
* @param feeRateSatPerKB - The fee rate to use for the transaction, in satoshis per kilobyte.
39+
*/
40+
async function sendOmniAsset(
41+
wallet: Wallet,
42+
receiver: string,
43+
sender: string,
44+
omniBaseAmount: bigint,
45+
assetId = 31,
46+
feeRateSatPerKB = 20_000
47+
) {
48+
if (!['1', '3', 'n', 'm'].includes(receiver.slice(0, 1))) {
49+
throw new Error(
50+
'Omni has only been verified to work with legacy and wrapped segwit addresses - use other address formats at your own risk'
51+
);
52+
}
53+
54+
const res = await superagent.get(`https://mempool.space/${omniConfig.MEMPOOL_PREFIX}api/address/${sender}/utxo`);
55+
const unspent = res.body[0];
56+
const unspent_id = unspent.txid + ':' + unspent.vout;
57+
58+
// scriptPubkey: op_return omni simple_send tether amount
59+
const transactionType = Buffer.alloc(4);
60+
const assetHex = Buffer.alloc(4);
61+
assetHex.writeUInt32BE(assetId);
62+
const amountHex = Buffer.alloc(8);
63+
amountHex.writeBigUInt64BE(omniBaseAmount);
64+
const omniScript = Buffer.concat([omniConfig.OMNI_PREFIX, transactionType, assetHex, amountHex]);
65+
const output = utxolib.payments.embed({ data: [omniScript], network: omniConfig.network }).output;
66+
if (!output) {
67+
throw new Error('Invalid output');
68+
}
69+
const script = output.toString('hex');
70+
const tx = await wallet.sendMany({
71+
recipients: [
72+
// this signals the receiver of the omni asset
73+
// we are not actually trying to send BTC to the receiver
74+
// so we send the minimum amount above the dust limit
75+
{
76+
amount: '546',
77+
address: receiver,
78+
},
79+
// this is the actual script that the omni layer reads for the send
80+
{
81+
amount: '0',
82+
address: `scriptPubkey:${script}`,
83+
},
84+
],
85+
isReplaceableByFee: true,
86+
feeRate: feeRateSatPerKB,
87+
walletPassphrase: omniConfig.walletPassphrase,
88+
// we must send change to our input address to ensure that omni won't
89+
// accidentally send our asset to the change address instead of the recipient
90+
changeAddress: sender,
91+
unspents: [unspent_id],
92+
});
93+
console.log('Omni asset sent: ', tx);
94+
}
95+
96+
/*
97+
* Usage: npx ts-node btc/omni/index.ts
98+
* */
99+
async function main() {
100+
console.log('Starting...');
101+
102+
const feeRateRes = await superagent.get(`https://mempool.space/${omniConfig.MEMPOOL_PREFIX}api/v1/fees/recommended`);
103+
const feeRate = feeRateRes.body.fastestFee;
104+
105+
const wallet = await getWallet();
106+
// we multiply feeRate by 1000 because mempool returns sat/vB and BitGo uses sat/kvB
107+
await sendOmniAsset(wallet, RECEIVE_ADDRESS, SEND_ADDRESS, BASE_AMOUNT, ASSET_ID, feeRate * 1000);
108+
}
109+
110+
main().catch((e) => {
111+
console.error(e);
112+
process.exit(1);
113+
});

0 commit comments

Comments
 (0)