Skip to content

Commit e810996

Browse files
Merge pull request #55 from BitGo/WP-4793/utxo-accelerate
feat(mbe): accelerate utxo
2 parents ae3c69b + 7cdcb99 commit e810996

File tree

6 files changed

+474
-28
lines changed

6 files changed

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

src/__tests__/api/master/consolidate.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,14 @@ describe('POST /api/:coin/wallet/:walletId/consolidate', () => {
223223
subType: 'onPrem',
224224
keys: ['user-key-id', 'backup-key-id', 'bitgo-key-id'],
225225
});
226+
// Mock keychain get request
227+
const keychainGetNock = nock(bitgoApiUrl)
228+
.get(`/api/v2/${coin}/key/user-key-id`)
229+
.matchHeader('any', () => true)
230+
.reply(200, {
231+
id: 'user-key-id',
232+
pub: 'xpub_user',
233+
});
226234

227235
// Mock allowsAccountConsolidations to return false
228236
const allowsConsolidationsStub = sinon
@@ -240,6 +248,7 @@ describe('POST /api/:coin/wallet/:walletId/consolidate', () => {
240248
response.status.should.equal(500);
241249

242250
walletGetNock.done();
251+
keychainGetNock.done();
243252
sinon.assert.calledOnce(allowsConsolidationsStub);
244253
});
245254

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import { RequestTracer, KeyIndices } from '@bitgo/sdk-core';
2+
import logger from '../../../logger';
3+
import { MasterApiSpecRouteRequest } from '../routers/masterApiSpec';
4+
import { getWalletAndSigningKeychain, makeCustomSigningFunction } from '../../../shared/coinUtils';
5+
6+
export async function handleAccelerate(
7+
req: MasterApiSpecRouteRequest<'v1.wallet.accelerate', 'post'>,
8+
) {
9+
const enclavedExpressClient = req.enclavedExpressClient;
10+
const reqId = new RequestTracer();
11+
const bitgo = req.bitgo;
12+
const params = req.decoded;
13+
const walletId = req.params.walletId;
14+
const coin = req.params.coin;
15+
16+
const { wallet, signingKeychain } = await getWalletAndSigningKeychain({
17+
bitgo,
18+
coin,
19+
walletId,
20+
params,
21+
reqId,
22+
KeyIndices,
23+
});
24+
25+
try {
26+
// Create custom signing function that delegates to EBE
27+
const customSigningFunction = makeCustomSigningFunction({
28+
enclavedExpressClient,
29+
source: params.source,
30+
pub: signingKeychain.pub!,
31+
});
32+
33+
// Prepare acceleration parameters
34+
const accelerationParams = {
35+
...params,
36+
customSigningFunction,
37+
reqId,
38+
};
39+
40+
// Accelerate transaction
41+
const result = await wallet.accelerateTransaction(accelerationParams);
42+
43+
return result;
44+
} catch (error) {
45+
const err = error as Error;
46+
logger.error('Failed to accelerate transaction: %s', err.message);
47+
throw err;
48+
}
49+
}

0 commit comments

Comments
 (0)