Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions modules/express/src/clientRoutes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -651,11 +651,11 @@ export async function handleV2GenerateWallet(req: express.Request) {
* handle new address creation
* @param req
*/
export async function handleV2CreateAddress(req: express.Request) {
export async function handleV2CreateAddress(req: ExpressApiRouteRequest<'express.v2.wallet.createAddress', 'post'>) {
const bitgo = req.bitgo;
const coin = bitgo.coin(req.params.coin);
const wallet = await coin.wallets().get({ id: req.params.id });
return wallet.createAddress(req.body);
const coin = bitgo.coin(req.decoded.coin);
const wallet = await coin.wallets().get({ id: req.decoded.id });
return wallet.createAddress(req.decoded);
}

/**
Expand Down Expand Up @@ -1617,8 +1617,7 @@ export function setupAPIRoutes(app: express.Application, config: Config): void {
promiseWrapper(handleKeychainChangePassword)
);

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

// share wallet
app.post('/api/v2/:coin/wallet/:id/share', parseBody, prepareBitGo(config), promiseWrapper(handleV2ShareWallet));
Expand Down
4 changes: 4 additions & 0 deletions modules/express/src/typedRoutes/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { PostDeriveLocalKeyChain } from './v1/deriveLocalKeyChain';
import { PostCreateLocalKeyChain } from './v1/createLocalKeyChain';
import { PutConstructPendingApprovalTx } from './v1/constructPendingApprovalTx';
import { PutConsolidateUnspents } from './v1/consolidateUnspents';
import { PostCreateAddress } from './v2/createAddress';
import { PutFanoutUnspents } from './v1/fanoutUnspents';
import { PostOfcSignPayload } from './v2/ofcSignPayload';

Expand Down Expand Up @@ -71,6 +72,9 @@ export const ExpressApi = apiSpec({
'express.verifycoinaddress': {
post: PostVerifyCoinAddress,
},
'express.v2.wallet.createAddress': {
post: PostCreateAddress,
},
'express.calculateminerfeeinfo': {
post: PostCalculateMinerFeeInfo,
},
Expand Down
76 changes: 76 additions & 0 deletions modules/express/src/typedRoutes/api/v2/createAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as t from 'io-ts';
import { httpRoute, httpRequest, optional } from '@api-ts/io-ts-http';
import { BitgoExpressError } from '../../schemas/error';
import { EIP1559, ForwarderVersion, CreateAddressFormat } from '../../schemas/address';

/**
* Path parameters for creating a wallet address
*/
export const CreateAddressParams = {
/** Coin ticker / chain identifier */
coin: t.string,
/** The ID of the wallet. */
id: t.string,
} as const;

/**
* Request body for creating a wallet address
*/
export const CreateAddressBody = {
/** Address type for chains that support multiple address types */
type: optional(t.string),
/** Chain on which the new address should be created. Default: 1 */
chain: optional(t.number),
/**
* (ETH only) Specify forwarder version to use in address creation.
* 0: legacy forwarder;
* 1: fee-improved forwarder;
* 2: NFT-supported forwarder (v2 wallets);
* 3: MPC wallets;
* 4: EVM variants;
* 5: new MPC wallets with wallet-version 6
*/
forwarderVersion: optional(ForwarderVersion),
/** EVM keyring reference address (EVM only) */
evmKeyRingReferenceAddress: optional(t.string),
/** Create an address for the given token (OFC only) (eg. ofcbtc) */
onToken: optional(t.string),
/** A human-readable label for the address (Max length: 250) */
label: optional(t.string),
/** Whether the deployment should use a low priority fee key (ETH only) Default: false */
lowPriority: optional(t.boolean),
/** Explicit gas price to use when deploying the forwarder contract (ETH only) */
gasPrice: optional(t.union([t.number, t.string])),
/** EIP1559 fee parameters (ETH forwarderVersion: 0 wallets only) */
eip1559: optional(EIP1559),
/** Format to use for the new address (e.g., 'cashaddr' for BCH) */
format: optional(CreateAddressFormat),
/** Number of new addresses to create (maximum 250) */
count: optional(t.number),
/** Base address of the wallet (if applicable) */
baseAddress: optional(t.string),
/** When false, throw error if address verification is skipped */
allowSkipVerifyAddress: optional(t.boolean),
} as const;

/** Response for creating a wallet address */
export const CreateAddressResponse = {
200: t.unknown,
400: BitgoExpressError,
} as const;

/**
* Create address for a wallet
*
* @tag express
* @operationId express.v2.wallet.createAddress
*/
export const PostCreateAddress = httpRoute({
path: '/api/v2/{coin}/wallet/{walletId}/address',
method: 'POST',
request: httpRequest({
params: CreateAddressParams,
body: CreateAddressBody,
}),
response: CreateAddressResponse,
});
10 changes: 10 additions & 0 deletions modules/express/src/typedRoutes/schemas/address.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import * as t from 'io-ts';

export const ForwarderVersion = t.union([t.literal(0), t.literal(1), t.literal(2), t.literal(3), t.literal(4)]);

export const EIP1559 = t.type({
maxFeePerGas: t.number,
maxPriorityFeePerGas: t.number,
});

export const CreateAddressFormat = t.union([t.literal('base58'), t.literal('cashaddr')]);
17 changes: 13 additions & 4 deletions modules/express/test/unit/clientRoutes/createAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import 'should-http';
import 'should-sinon';
import '../../lib/asserts';

import * as express from 'express';

import { handleV2CreateAddress } from '../../../src/clientRoutes';

import { BitGo } from 'bitgo';
import { ExpressApiRouteRequest } from '../../../src/typedRoutes/api';
import { CreateAddressResponse } from '../../../src/typedRoutes/api/v2/createAddress';
import { decodeOrElse } from '@bitgo/sdk-core';

describe('Create Address', () => {
function createAddressMocks(res) {
Expand Down Expand Up @@ -36,8 +37,16 @@ describe('Create Address', () => {
body: {
chain: 0,
},
} as unknown as express.Request;
decoded: {
coin: 'tbtc',
id: '23423423423423',
chain: 0,
},
} as unknown as ExpressApiRouteRequest<'express.v2.wallet.createAddress', 'post'>;

await handleV2CreateAddress(req).should.be.resolvedWith(res);
const result = await handleV2CreateAddress(req).should.be.resolvedWith(res);
decodeOrElse('express.v2.wallet.createAddress', CreateAddressResponse[200], result, (errors) => {
throw new Error(`Response did not match expected codec: ${errors}`);
});
});
});
20 changes: 20 additions & 0 deletions modules/express/test/unit/typedRoutes/decode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '../../../src/typedRoutes/api/v2/lightningInitWallet';
import { UnlockLightningWalletBody, UnlockLightningWalletParams } from '../../../src/typedRoutes/api/v2/unlockWallet';
import { OfcSignPayloadBody } from '../../../src/typedRoutes/api/v2/ofcSignPayload';
import { CreateAddressBody, CreateAddressParams } from '../../../src/typedRoutes/api/v2/createAddress';

export function assertDecode<T>(codec: t.Type<T, unknown>, input: unknown): T {
const result = codec.decode(input);
Expand Down Expand Up @@ -223,4 +224,23 @@ describe('io-ts decode tests', function () {
walletPassphrase: 'secret',
});
});

it('express.v2.wallet.createAddress params', function () {
// missing id
assert.throws(() => assertDecode(t.type(CreateAddressParams), { coin: 'btc' }));
// coin must be string
assert.throws(() =>
assertDecode(t.type(CreateAddressParams), { coin: 123, id: '59cd72485007a239fb00282ed480da1f' })
);
// id must be string
assert.throws(() => assertDecode(t.type(CreateAddressParams), { coin: 'btc', id: 123 }));
// valid params
assertDecode(t.type(CreateAddressParams), { coin: 'btc', id: '59cd72485007a239fb00282ed480da1f' });
// invalid body
assert.throws(() => assertDecode(t.type(CreateAddressBody), { chain: '1' }));
assert.throws(() => assertDecode(t.type(CreateAddressBody), { format: 'invalid' }));
// valid body
assertDecode(t.type(CreateAddressBody), { eip1559: { maxFeePerGas: 1, maxPriorityFeePerGas: 1 } });
assertDecode(t.type(CreateAddressBody), {});
});
});