Skip to content

Commit f1e2efb

Browse files
feat(express): migrate createAddress to typed routes
2 parents 3b25b96 + 2d19b09 commit f1e2efb

File tree

6 files changed

+128
-10
lines changed

6 files changed

+128
-10
lines changed

modules/express/src/clientRoutes.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -651,11 +651,11 @@ export async function handleV2GenerateWallet(req: express.Request) {
651651
* handle new address creation
652652
* @param req
653653
*/
654-
export async function handleV2CreateAddress(req: express.Request) {
654+
export async function handleV2CreateAddress(req: ExpressApiRouteRequest<'express.v2.wallet.createAddress', 'post'>) {
655655
const bitgo = req.bitgo;
656-
const coin = bitgo.coin(req.params.coin);
657-
const wallet = await coin.wallets().get({ id: req.params.id });
658-
return wallet.createAddress(req.body);
656+
const coin = bitgo.coin(req.decoded.coin);
657+
const wallet = await coin.wallets().get({ id: req.decoded.id });
658+
return wallet.createAddress(req.decoded);
659659
}
660660

661661
/**
@@ -1617,8 +1617,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
16171617
promiseWrapper(handleKeychainChangePassword)
16181618
);
16191619

1620-
// create address
1621-
app.post('/api/v2/:coin/wallet/:id/address', parseBody, prepareBitGo(config), promiseWrapper(handleV2CreateAddress));
1620+
router.post('express.v2.wallet.createAddress', [prepareBitGo(config), typedPromiseWrapper(handleV2CreateAddress)]);
16221621

16231622
// share wallet
16241623
app.post('/api/v2/:coin/wallet/:id/share', parseBody, prepareBitGo(config), promiseWrapper(handleV2ShareWallet));

modules/express/src/typedRoutes/api/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { PostDeriveLocalKeyChain } from './v1/deriveLocalKeyChain';
2222
import { PostCreateLocalKeyChain } from './v1/createLocalKeyChain';
2323
import { PutConstructPendingApprovalTx } from './v1/constructPendingApprovalTx';
2424
import { PutConsolidateUnspents } from './v1/consolidateUnspents';
25+
import { PostCreateAddress } from './v2/createAddress';
2526
import { PutFanoutUnspents } from './v1/fanoutUnspents';
2627
import { PostOfcSignPayload } from './v2/ofcSignPayload';
2728

@@ -71,6 +72,9 @@ export const ExpressApi = apiSpec({
7172
'express.verifycoinaddress': {
7273
post: PostVerifyCoinAddress,
7374
},
75+
'express.v2.wallet.createAddress': {
76+
post: PostCreateAddress,
77+
},
7478
'express.calculateminerfeeinfo': {
7579
post: PostCalculateMinerFeeInfo,
7680
},
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as t from 'io-ts';
2+
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
3+
import { BitgoExpressError } from '../../schemas/error';
4+
import { EIP1559, ForwarderVersion, CreateAddressFormat } from '../../schemas/address';
5+
6+
/**
7+
* Path parameters for creating a wallet address
8+
*/
9+
export const CreateAddressParams = {
10+
/** Coin ticker / chain identifier */
11+
coin: t.string,
12+
/** The ID of the wallet. */
13+
id: t.string,
14+
} as const;
15+
16+
/**
17+
* Request body for creating a wallet address
18+
*/
19+
export const CreateAddressBody = {
20+
/** Address type for chains that support multiple address types */
21+
type: optional(t.string),
22+
/** Chain on which the new address should be created. Default: 1 */
23+
chain: optional(t.number),
24+
/**
25+
* (ETH only) Specify forwarder version to use in address creation.
26+
* 0: legacy forwarder;
27+
* 1: fee-improved forwarder;
28+
* 2: NFT-supported forwarder (v2 wallets);
29+
* 3: MPC wallets;
30+
* 4: EVM variants;
31+
* 5: new MPC wallets with wallet-version 6
32+
*/
33+
forwarderVersion: optional(ForwarderVersion),
34+
/** EVM keyring reference address (EVM only) */
35+
evmKeyRingReferenceAddress: optional(t.string),
36+
/** Create an address for the given token (OFC only) (eg. ofcbtc) */
37+
onToken: optional(t.string),
38+
/** A human-readable label for the address (Max length: 250) */
39+
label: optional(t.string),
40+
/** Whether the deployment should use a low priority fee key (ETH only) Default: false */
41+
lowPriority: optional(t.boolean),
42+
/** Explicit gas price to use when deploying the forwarder contract (ETH only) */
43+
gasPrice: optional(t.union([t.number, t.string])),
44+
/** EIP1559 fee parameters (ETH forwarderVersion: 0 wallets only) */
45+
eip1559: optional(EIP1559),
46+
/** Format to use for the new address (e.g., 'cashaddr' for BCH) */
47+
format: optional(CreateAddressFormat),
48+
/** Number of new addresses to create (maximum 250) */
49+
count: optional(t.number),
50+
/** Base address of the wallet (if applicable) */
51+
baseAddress: optional(t.string),
52+
/** When false, throw error if address verification is skipped */
53+
allowSkipVerifyAddress: optional(t.boolean),
54+
} as const;
55+
56+
/** Response for creating a wallet address */
57+
export const CreateAddressResponse = {
58+
200: t.unknown,
59+
400: BitgoExpressError,
60+
} as const;
61+
62+
/**
63+
* Create address for a wallet
64+
*
65+
* @tag express
66+
* @operationId express.v2.wallet.createAddress
67+
*/
68+
export const PostCreateAddress = httpRoute({
69+
path: '/api/v2/{coin}/wallet/{walletId}/address',
70+
method: 'POST',
71+
request: httpRequest({
72+
params: CreateAddressParams,
73+
body: CreateAddressBody,
74+
}),
75+
response: CreateAddressResponse,
76+
});
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import * as t from 'io-ts';
2+
3+
export const ForwarderVersion = t.union([t.literal(0), t.literal(1), t.literal(2), t.literal(3), t.literal(4)]);
4+
5+
export const EIP1559 = t.type({
6+
maxFeePerGas: t.number,
7+
maxPriorityFeePerGas: t.number,
8+
});
9+
10+
export const CreateAddressFormat = t.union([t.literal('base58'), t.literal('cashaddr')]);

modules/express/test/unit/clientRoutes/createAddress.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ import 'should-http';
44
import 'should-sinon';
55
import '../../lib/asserts';
66

7-
import * as express from 'express';
8-
97
import { handleV2CreateAddress } from '../../../src/clientRoutes';
108

119
import { BitGo } from 'bitgo';
10+
import { ExpressApiRouteRequest } from '../../../src/typedRoutes/api';
11+
import { CreateAddressResponse } from '../../../src/typedRoutes/api/v2/createAddress';
12+
import { decodeOrElse } from '@bitgo/sdk-core';
1213

1314
describe('Create Address', () => {
1415
function createAddressMocks(res) {
@@ -36,8 +37,16 @@ describe('Create Address', () => {
3637
body: {
3738
chain: 0,
3839
},
39-
} as unknown as express.Request;
40+
decoded: {
41+
coin: 'tbtc',
42+
id: '23423423423423',
43+
chain: 0,
44+
},
45+
} as unknown as ExpressApiRouteRequest<'express.v2.wallet.createAddress', 'post'>;
4046

41-
await handleV2CreateAddress(req).should.be.resolvedWith(res);
47+
const result = await handleV2CreateAddress(req).should.be.resolvedWith(res);
48+
decodeOrElse('express.v2.wallet.createAddress', CreateAddressResponse[200], result, (errors) => {
49+
throw new Error(`Response did not match expected codec: ${errors}`);
50+
});
4251
});
4352
});

modules/express/test/unit/typedRoutes/decode.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
} from '../../../src/typedRoutes/api/v2/lightningInitWallet';
1515
import { UnlockLightningWalletBody, UnlockLightningWalletParams } from '../../../src/typedRoutes/api/v2/unlockWallet';
1616
import { OfcSignPayloadBody } from '../../../src/typedRoutes/api/v2/ofcSignPayload';
17+
import { CreateAddressBody, CreateAddressParams } from '../../../src/typedRoutes/api/v2/createAddress';
1718

1819
export function assertDecode<T>(codec: t.Type<T, unknown>, input: unknown): T {
1920
const result = codec.decode(input);
@@ -223,4 +224,23 @@ describe('io-ts decode tests', function () {
223224
walletPassphrase: 'secret',
224225
});
225226
});
227+
228+
it('express.v2.wallet.createAddress params', function () {
229+
// missing id
230+
assert.throws(() => assertDecode(t.type(CreateAddressParams), { coin: 'btc' }));
231+
// coin must be string
232+
assert.throws(() =>
233+
assertDecode(t.type(CreateAddressParams), { coin: 123, id: '59cd72485007a239fb00282ed480da1f' })
234+
);
235+
// id must be string
236+
assert.throws(() => assertDecode(t.type(CreateAddressParams), { coin: 'btc', id: 123 }));
237+
// valid params
238+
assertDecode(t.type(CreateAddressParams), { coin: 'btc', id: '59cd72485007a239fb00282ed480da1f' });
239+
// invalid body
240+
assert.throws(() => assertDecode(t.type(CreateAddressBody), { chain: '1' }));
241+
assert.throws(() => assertDecode(t.type(CreateAddressBody), { format: 'invalid' }));
242+
// valid body
243+
assertDecode(t.type(CreateAddressBody), { eip1559: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 } });
244+
assertDecode(t.type(CreateAddressBody), {});
245+
});
226246
});

0 commit comments

Comments
 (0)