Skip to content

Commit 20cefe5

Browse files
Merge pull request #6844 from BitGo/BTC-2425
feat(sdk-core): add walletUtil module with UTXO message proof
2 parents ab89764 + dc853e6 commit 20cefe5

File tree

3 files changed

+130
-0
lines changed

3 files changed

+130
-0
lines changed

modules/sdk-core/src/bitgo/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import * as bitcoinUtil from './bitcoin';
22
import * as tss from './tss';
33

4+
export * as walletUtil from './walletUtil';
45
export * from './baseCoin';
56
export * from './bip32util';
67
export * from './bitcoin';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './utxoMessageProof';
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import * as utxolib from '@bitgo/utxo-lib';
2+
3+
import { IWallet } from '../wallet/iWallet';
4+
import { Environments } from '../environments';
5+
6+
const NUM_MESSAGES_PER_TRANSACTION = 2;
7+
const NUM_MESSAGES_PER_QUERY = 1000;
8+
9+
type MessageInfo = {
10+
message: string;
11+
address: string;
12+
};
13+
14+
type Claim = {
15+
originWalletId: string;
16+
status: string;
17+
originAddress?: string;
18+
allocationAmount: string;
19+
};
20+
21+
type BulkMessageResponse = {
22+
success: boolean;
23+
numMessages: number;
24+
transactions: Record<string, unknown>[];
25+
};
26+
27+
export interface IMessageProvider {
28+
/**
29+
* Returns the messages and addresses that we want to sign. We call this function multiple times until there are no more
30+
* messages. If there are no more messages, an empty array is returned. Note that we only return messages in sets of 200.
31+
*/
32+
getMessagesAndAddressesToSign(): Promise<MessageInfo[]>;
33+
}
34+
35+
/**
36+
* The Midnight drop service can return up to 1000 messages per request. However, UTXO coins
37+
* can only have a maximum of 200 messages per transaction. We make this wrapper function that
38+
* handles the pagination and batching of messages, keeping a local cache of the unprocessed messages.
39+
*/
40+
export class MidnightMessageProvider implements IMessageProvider {
41+
protected messageCache: MessageInfo[];
42+
protected network: utxolib.Network;
43+
protected midnightClaimUrl: string;
44+
protected prevId: string | undefined;
45+
protected ranOnce = false;
46+
constructor(private wallet: IWallet, private message: string) {
47+
this.messageCache = [];
48+
this.network = utxolib.networks[wallet.coin()];
49+
this.midnightClaimUrl = `${
50+
Environments[wallet.bitgo.env].uri
51+
}/api/airdrop-claim/v1/midnight/claims/${wallet.coin()}/${wallet.id()}`;
52+
}
53+
54+
async getMessagesAndAddressesToSign(): Promise<MessageInfo[]> {
55+
if (this.messageCache.length > 0) {
56+
return this.messageCache.splice(0, NUM_MESSAGES_PER_TRANSACTION);
57+
} else if (this.messageCache.length === 0 && this.ranOnce && this.prevId === undefined) {
58+
return [];
59+
}
60+
61+
this.ranOnce = true;
62+
const query: Record<string, unknown> = {
63+
status: 'UNINITIALIZED',
64+
limit: NUM_MESSAGES_PER_QUERY,
65+
};
66+
if (this.prevId !== undefined) {
67+
query.prevId = this.prevId;
68+
}
69+
const response = await this.wallet.bitgo.get(this.midnightClaimUrl).query(query).result();
70+
if (response.status !== 'success') {
71+
throw new Error(`Unexpected status code ${response.status} from ${this.midnightClaimUrl}`);
72+
}
73+
if (response?.pagination?.hasNext) {
74+
this.prevId = response?.pagination?.nextPrevId;
75+
} else {
76+
this.prevId = undefined;
77+
}
78+
79+
this.messageCache = response.claims.map((claim: Claim) => {
80+
if (!claim.originAddress) {
81+
throw new Error(`Claim ${JSON.stringify(claim)} is missing originAddress`);
82+
}
83+
return {
84+
message: this.message,
85+
address: claim.originAddress,
86+
};
87+
});
88+
const toReturn = this.messageCache.splice(0, NUM_MESSAGES_PER_TRANSACTION);
89+
return toReturn;
90+
}
91+
}
92+
93+
export async function bulkSignBip322MidnightMessages(
94+
wallet: IWallet,
95+
message: string,
96+
walletPassphrase?: string
97+
): Promise<BulkMessageResponse> {
98+
const provider = new MidnightMessageProvider(wallet, message);
99+
return bulkSignBip322MessagesWithProvider(provider, wallet, walletPassphrase);
100+
}
101+
102+
async function bulkSignBip322MessagesWithProvider(
103+
provider: IMessageProvider,
104+
wallet: IWallet,
105+
walletPassphrase?: string
106+
): Promise<BulkMessageResponse> {
107+
let numMessages = 0;
108+
let messages: MessageInfo[] = await provider.getMessagesAndAddressesToSign();
109+
const sendingFunction = wallet.type() === 'cold' ? wallet.prebuildTransaction : wallet.sendMany;
110+
const transactions: Record<string, unknown>[] = [];
111+
while (messages.length > 0) {
112+
// Sign the messages with the wallet
113+
const result = await sendingFunction.call(wallet, {
114+
messages,
115+
// Recipients must be empty
116+
recipients: [],
117+
// txFormat must be psbt
118+
txFormat: 'psbt',
119+
// Pass in the optional wallet passphrase
120+
walletPassphrase,
121+
offlineVerification: wallet.type() === 'cold',
122+
});
123+
transactions.push(result);
124+
numMessages += messages.length;
125+
messages = await provider.getMessagesAndAddressesToSign();
126+
}
127+
return { success: true, numMessages, transactions };
128+
}

0 commit comments

Comments
 (0)