Skip to content

Commit 7bdffed

Browse files
feat(mbe): accelerate utxo
1 parent ae3c69b commit 7bdffed

File tree

3 files changed

+400
-0
lines changed

3 files changed

+400
-0
lines changed
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
import 'should';
2+
import sinon from 'sinon';
3+
import * as request from 'supertest';
4+
import nock from 'nock';
5+
import { app as expressApp } from '../../../masterExpressApp';
6+
import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types';
7+
import { Environments, Wallet } from '@bitgo/sdk-core';
8+
import { Tbtc } from '@bitgo/sdk-coin-btc';
9+
10+
describe('POST /api/:coin/wallet/:walletId/accelerate', () => {
11+
let agent: request.SuperAgentTest;
12+
const coin = 'tbtc';
13+
const walletId = 'test-wallet-id';
14+
const accessToken = 'test-access-token';
15+
const bitgoApiUrl = Environments.test.uri;
16+
const enclavedExpressUrl = 'https://test-enclaved-express.com';
17+
18+
before(() => {
19+
nock.disableNetConnect();
20+
nock.enableNetConnect('127.0.0.1');
21+
22+
const config: MasterExpressConfig = {
23+
appMode: AppMode.MASTER_EXPRESS,
24+
port: 0, // Let OS assign a free port
25+
bind: 'localhost',
26+
timeout: 30000,
27+
logFile: '',
28+
env: 'test',
29+
disableEnvCheck: true,
30+
authVersion: 2,
31+
enclavedExpressUrl: enclavedExpressUrl,
32+
enclavedExpressCert: 'test-cert',
33+
tlsMode: TlsMode.DISABLED,
34+
mtlsRequestCert: false,
35+
allowSelfSigned: true,
36+
};
37+
38+
const app = expressApp(config);
39+
agent = request.agent(app);
40+
});
41+
42+
afterEach(() => {
43+
nock.cleanAll();
44+
sinon.restore();
45+
});
46+
47+
it('should accelerate transaction by calling the enclaved express service', async () => {
48+
// Mock wallet get request
49+
const walletGetNock = nock(bitgoApiUrl)
50+
.get(`/api/v2/${coin}/wallet/${walletId}`)
51+
.matchHeader('any', () => true)
52+
.reply(200, {
53+
id: walletId,
54+
type: 'cold',
55+
subType: 'onPrem',
56+
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
57+
});
58+
59+
// Mock keychain get request
60+
const keychainGetNock = nock(bitgoApiUrl)
61+
.get(`/api/v2/${coin}/key/user-key-id`)
62+
.matchHeader('any', () => true)
63+
.reply(200, {
64+
id: 'user-key-id',
65+
pub: 'xpub_user',
66+
});
67+
68+
// Mock accelerateTransaction
69+
const accelerateTransactionStub = sinon
70+
.stub(Wallet.prototype, 'accelerateTransaction')
71+
.resolves({
72+
txid: 'accelerated-tx-id',
73+
tx: "accerated-transaction-hex",
74+
status: 'signed',
75+
});
76+
77+
const response = await agent
78+
.post(`/api/${coin}/wallet/${walletId}/accelerate`)
79+
.set('Authorization', `Bearer ${accessToken}`)
80+
.send({
81+
source: 'user',
82+
pubkey: 'xpub_user',
83+
cpfpTxIds: ['b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26'],
84+
cpfpFeeRate: 50,
85+
maxFee: 10000,
86+
});
87+
88+
response.status.should.equal(200);
89+
response.body.should.have.property('txid', 'accelerated-tx-id');
90+
response.body.should.have.property('status', 'signed');
91+
92+
walletGetNock.done();
93+
keychainGetNock.done();
94+
sinon.assert.calledOnce(accelerateTransactionStub);
95+
});
96+
97+
it('should handle acceleration with backup key signing', async () => {
98+
// Mock wallet get request
99+
const walletGetNock = nock(bitgoApiUrl)
100+
.get(`/api/v2/${coin}/wallet/${walletId}`)
101+
.matchHeader('any', () => true)
102+
.reply(200, {
103+
id: walletId,
104+
type: 'cold',
105+
subType: 'onPrem',
106+
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
107+
});
108+
109+
// Mock keychain get request for backup key
110+
const keychainGetNock = nock(bitgoApiUrl)
111+
.get(`/api/v2/${coin}/key/backup-key-id`)
112+
.matchHeader('any', () => true)
113+
.reply(200, {
114+
id: 'backup-key-id',
115+
pub: 'xpub_backup',
116+
});
117+
118+
// Mock accelerateTransaction
119+
const accelerateTransactionStub = sinon
120+
.stub(Wallet.prototype, 'accelerateTransaction')
121+
.resolves({
122+
txid: 'accelerated-tx-id',
123+
status: 'signed',
124+
tx: "accelerated-transaction-hex",
125+
});
126+
127+
const response = await agent
128+
.post(`/api/${coin}/wallet/${walletId}/accelerate`)
129+
.set('Authorization', `Bearer ${accessToken}`)
130+
.send({
131+
source: 'backup',
132+
pubkey: 'xpub_backup',
133+
rbfTxIds: ['b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26'],
134+
feeMultiplier: 1.5,
135+
});
136+
137+
response.status.should.equal(200);
138+
response.body.should.have.property('txid', 'accelerated-tx-id');
139+
140+
walletGetNock.done();
141+
keychainGetNock.done();
142+
sinon.assert.calledOnce(accelerateTransactionStub);
143+
});
144+
145+
it('should throw error when wallet not found', async () => {
146+
// Mock wallet get request to return 404
147+
const walletGetNock = nock(bitgoApiUrl)
148+
.get(`/api/v2/${coin}/wallet/${walletId}`)
149+
.matchHeader('any', () => true)
150+
.reply(404, { error: 'Wallet not found' });
151+
152+
const response = await agent
153+
.post(`/api/${coin}/wallet/${walletId}/accelerate`)
154+
.set('Authorization', `Bearer ${accessToken}`)
155+
.send({
156+
source: 'user',
157+
pubkey: 'xpub_user',
158+
cpfpTxIds: ['b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26'],
159+
});
160+
161+
response.status.should.equal(500);
162+
response.body.should.have.property('error');
163+
164+
walletGetNock.done();
165+
});
166+
167+
it('should throw error when signing keychain not found', async () => {
168+
// Mock wallet get request
169+
const walletGetNock = nock(bitgoApiUrl)
170+
.get(`/api/v2/${coin}/wallet/${walletId}`)
171+
.matchHeader('any', () => true)
172+
.reply(200, {
173+
id: walletId,
174+
type: 'cold',
175+
subType: 'onPrem',
176+
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
177+
});
178+
179+
// Mock keychain get request to return 404
180+
const keychainGetNock = nock(bitgoApiUrl)
181+
.get(`/api/v2/${coin}/key/user-key-id`)
182+
.matchHeader('any', () => true)
183+
.reply(404, { error: 'Keychain not found' });
184+
185+
const response = await agent
186+
.post(`/api/${coin}/wallet/${walletId}/accelerate`)
187+
.set('Authorization', `Bearer ${accessToken}`)
188+
.send({
189+
source: 'user',
190+
pubkey: 'xpub_user',
191+
cpfpTxIds: ['b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26'],
192+
});
193+
194+
response.status.should.equal(500);
195+
response.body.should.have.property('error');
196+
197+
walletGetNock.done();
198+
keychainGetNock.done();
199+
});
200+
201+
it('should throw error when provided pubkey does not match wallet keychain', async () => {
202+
// Mock wallet get request
203+
const walletGetNock = nock(bitgoApiUrl)
204+
.get(`/api/v2/${coin}/wallet/${walletId}`)
205+
.matchHeader('any', () => true)
206+
.reply(200, {
207+
id: walletId,
208+
type: 'cold',
209+
subType: 'onPrem',
210+
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
211+
});
212+
213+
// Mock keychain get request
214+
const keychainGetNock = nock(bitgoApiUrl)
215+
.get(`/api/v2/${coin}/key/user-key-id`)
216+
.matchHeader('any', () => true)
217+
.reply(200, {
218+
id: 'user-key-id',
219+
pub: 'xpub_user',
220+
});
221+
222+
const response = await agent
223+
.post(`/api/${coin}/wallet/${walletId}/accelerate`)
224+
.set('Authorization', `Bearer ${accessToken}`)
225+
.send({
226+
source: 'user',
227+
pubkey: 'wrong_pubkey',
228+
cpfpTxIds: ['b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26'],
229+
});
230+
231+
response.status.should.equal(500);
232+
response.body.should.have.property('error');
233+
234+
walletGetNock.done();
235+
keychainGetNock.done();
236+
});
237+
238+
it('should handle acceleration with additional parameters', async () => {
239+
// Mock wallet get request
240+
const walletGetNock = nock(bitgoApiUrl)
241+
.get(`/api/v2/${coin}/wallet/${walletId}`)
242+
.matchHeader('any', () => true)
243+
.reply(200, {
244+
id: walletId,
245+
type: 'cold',
246+
subType: 'onPrem',
247+
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
248+
});
249+
250+
// Mock keychain get request
251+
const keychainGetNock = nock(bitgoApiUrl)
252+
.get(`/api/v2/${coin}/key/user-key-id`)
253+
.matchHeader('any', () => true)
254+
.reply(200, {
255+
id: 'user-key-id',
256+
pub: 'xpub_user',
257+
});
258+
259+
// Mock accelerateTransaction
260+
const accelerateTransactionStub = sinon
261+
.stub(Wallet.prototype, 'accelerateTransaction')
262+
.resolves({
263+
txid: 'accelerated-tx-id',
264+
status: 'signed',
265+
tx: "accelerated-transaction-hex",
266+
});
267+
268+
const response = await agent
269+
.post(`/api/${coin}/wallet/${walletId}/accelerate`)
270+
.set('Authorization', `Bearer ${accessToken}`)
271+
.send({
272+
source: 'user',
273+
pubkey: 'xpub_user',
274+
cpfpTxIds: ['b8a828b98dbf32d9fd1875cbace9640ceb8c82626716b4a64203fdc79bb46d26'],
275+
cpfpFeeRate: 100,
276+
maxFee: 20000,
277+
feeMultiplier: 2.0,
278+
});
279+
280+
response.status.should.equal(200);
281+
response.body.should.have.property('txid', 'accelerated-tx-id');
282+
283+
walletGetNock.done();
284+
keychainGetNock.done();
285+
sinon.assert.calledOnce(accelerateTransactionStub);
286+
});
287+
});
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import { RequestTracer, KeyIndices } from '@bitgo/sdk-core';
2+
import logger from '../../../logger';
3+
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
4+
5+
export async function handleAccelerate(
6+
req: MasterApiSpecRouteRequest<'v1.wallet.accelerate', 'post'>,
7+
) {
8+
const enclavedExpressClient = req.enclavedExpressClient;
9+
const reqId = new RequestTracer();
10+
const bitgo = req.bitgo;
11+
const baseCoin = bitgo.coin(req.params.coin);
12+
const params = req.decoded;
13+
const walletId = req.params.walletId;
14+
const wallet = await baseCoin.wallets().get({ id: walletId, reqId });
15+
16+
// Log the runtime class name of the wallet object
17+
logger.info('Wallet runtime class name: %s', wallet?.constructor.name);
18+
logger.info('Wallet prototype chain: %s', Object.getPrototypeOf(wallet)?.constructor.name);
19+
20+
if (!wallet) {
21+
throw new Error(`Wallet ${walletId} not found`);
22+
}
23+
24+
// Get the signing keychain based on source
25+
const keyIdIndex = params.source === 'user' ? KeyIndices.USER : KeyIndices.BACKUP;
26+
const signingKeychain = await baseCoin.keychains().get({
27+
id: wallet.keyIds()[keyIdIndex],
28+
});
29+
30+
if (!signingKeychain || !signingKeychain.pub) {
31+
throw new Error(`Signing keychain for ${params.source} not found`);
32+
}
33+
34+
if (params.pubkey && params.pubkey !== signingKeychain.pub) {
35+
throw new Error(`Pub provided does not match the keychain on wallet for ${params.source}`);
36+
}
37+
38+
try {
39+
// Create custom signing function that delegates to EBE
40+
const customSigningFunction = async (signParams: any) => {
41+
const signedTx = await enclavedExpressClient.signMultisig({
42+
txPrebuild: signParams.txPrebuild,
43+
source: params.source,
44+
pub: signingKeychain.pub!,
45+
});
46+
return signedTx;
47+
};
48+
49+
// Prepare acceleration parameters
50+
const accelerationParams = {
51+
...params,
52+
customSigningFunction,
53+
reqId,
54+
};
55+
56+
// Accelerate transaction
57+
const result = await wallet.accelerateTransaction(accelerationParams);
58+
59+
return result;
60+
} catch (error) {
61+
const err = error as Error;
62+
logger.error('Failed to accelerate transaction: %s', err.message);
63+
throw err;
64+
}
65+
}

0 commit comments

Comments
 (0)