Skip to content
Closed
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
9 changes: 8 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,14 @@ module.exports = {
rules: {
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }],
'@typescript-eslint/no-unused-vars': [
'error',
{
argsIgnorePattern: '^_',
varsIgnorePattern: '^_',
caughtErrorsIgnorePattern: '^_',
},
],
},
ignorePatterns: ['dist/**/*', 'node_modules/**/*'],
};
11,090 changes: 9,688 additions & 1,402 deletions package-lock.json

Large diffs are not rendered by default.

18 changes: 10 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@
"@api-ts/superagent-wrapper": "^1.3.3",
"@api-ts/typed-express-router": "2.0.0",
"@bitgo-beta/abstract-cosmos": "1.0.1-beta.1741",
"@bitgo-beta/abstract-eth": "1.0.2-beta.1990",
"@bitgo-beta/abstract-utxo": "1.1.1-beta.1993",
"@bitgo-beta/sdk-api": "1.10.1-beta.1759",
"@bitgo-beta/abstract-eth": "1.0.2-beta.2004",
"@bitgo-beta/abstract-utxo": "1.1.1-beta.2007",
"@bitgo-beta/sdk-api": "1.10.1-beta.1773",
"@bitgo-beta/sdk-coin-ada": "2.3.14-beta.1758",
"@bitgo-beta/sdk-coin-algo": "2.8.9-beta.238",
"@bitgo-beta/sdk-coin-apt": "1.0.1-beta.1200",
Expand Down Expand Up @@ -59,8 +59,8 @@
"@bitgo-beta/sdk-coin-dot": "2.2.8-beta.1756",
"@bitgo-beta/sdk-coin-eos": "1.3.19-beta.1754",
"@bitgo-beta/sdk-coin-etc": "1.0.2-beta.1982",
"@bitgo-beta/sdk-coin-eth": "4.4.1-beta.1754",
"@bitgo-beta/sdk-coin-ethw": "20.0.76-beta.921",
"@bitgo-beta/sdk-coin-eth": "4.4.1-beta.1768",
"@bitgo-beta/sdk-coin-ethw": "20.0.76-beta.935",
"@bitgo-beta/sdk-coin-flr": "1.0.1-beta.1098",
"@bitgo-beta/sdk-coin-hash": "1.0.1-beta.1714",
"@bitgo-beta/sdk-coin-hbar": "1.0.2-beta.1984",
Expand Down Expand Up @@ -99,9 +99,9 @@
"@bitgo-beta/sdk-coin-zec": "1.1.1-beta.1984",
"@bitgo-beta/sdk-coin-zeta": "1.0.1-beta.1675",
"@bitgo-beta/sdk-coin-zketh": "1.0.1-beta.1540",
"@bitgo-beta/sdk-core": "8.2.1-beta.1760",
"@bitgo-beta/sdk-lib-mpc": "8.2.0-beta.1755",
"@bitgo-beta/statics": "15.1.1-beta.1767",
"@bitgo-beta/sdk-core": "8.2.1-beta.1775",
"@bitgo-beta/sdk-lib-mpc": "8.2.0-beta.1770",
"@bitgo-beta/statics": "15.1.1-beta.1782",
"@bitgo/wasm-miniscript": "2.0.0-beta.7",
"@commitlint/config-conventional": "^19.8.1",
"@ethereumjs/tx": "^3.3.0",
Expand All @@ -120,6 +120,8 @@
"zod": "^3.25.48"
},
"overrides": {
"@bitgo-beta/sdk-core": "8.2.1-beta.1775",
"@bitgo-beta/statics": "15.1.1-beta.1782",
"elliptic": "^6.6.1",
"expo": "^48.0.0",
"form-data": "^4.0.4",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,12 @@ describe('postIndependentKey — external signing mode', () => {
keychains: () => ({ create: sinon.stub().returns({ pub: 'xpub...', prv: 'xprv...' }) }),
} as unknown as BaseCoin;

const unsupportedExternalCoinStub = {
getFamily: () => CoinFamily.XRP,
getFullName: () => 'Test XRP',
keychains: () => ({ create: sinon.stub().returns({ pub: 'xpub...', prv: 'xprv...' }) }),
} as unknown as BaseCoin;

before(() => {
nock.disableNetConnect();
nock.enableNetConnect('127.0.0.1');
Expand Down Expand Up @@ -190,8 +196,26 @@ describe('postIndependentKey — external signing mode', () => {
createSpy.called.should.equal(false);
});

it('should fall through to local path for non-UTXO coin in external mode', async () => {
it('should call POST /key/generate for ETH coin and not call POST /key', async () => {
coinStub = sinon.stub(coinFactory, 'getCoin').resolves(nonUtxoCoinStub);
const externalKeyGeneratorNock = nock(keyProviderUrl)
.post('/key/generate', { coin: 'hteth', source: 'user', type: 'independent' })
.reply(200, { ...mockGenerateKeyResponse, coin: 'hteth' });
const localKeyGeneratorNock = nock(keyProviderUrl).post('/key').reply(200, {});

const response = await agent
.post(`/api/hteth/key/independent`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ source: 'user' });

response.status.should.equal(200);
response.body.should.have.property('pub', mockGenerateKeyResponse.pub);
externalKeyGeneratorNock.done();
localKeyGeneratorNock.isDone().should.equal(false);
});

it('should fall through to local path for unsupported external coin in external mode', async () => {
coinStub = sinon.stub(coinFactory, 'getCoin').resolves(unsupportedExternalCoinStub);
const externalKeyGeneratorNock = nock(keyProviderUrl).post('/key/generate').reply(200, {});
nock(keyProviderUrl).post('/key').reply(200, mockGenerateKeyResponse);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,11 +181,9 @@ describe('signMultisigTransaction — external signing mode', () => {
getFullName: () => 'Test Bitcoin',
} as unknown as BaseCoin;

coinStub = sinon.stub(coinFactory, 'getCoin').resolves(utxoCoinStub);

const nonUtxoCoinStub = {
getFamily: () => CoinFamily.ETH,
getFullName: () => 'Test Ethereum',
const unsupportedCoinStub = {
getFamily: () => CoinFamily.XRP,
getFullName: () => 'Test XRP',
} as unknown as BaseCoin;

before(() => {
Expand Down Expand Up @@ -231,8 +229,8 @@ describe('signMultisigTransaction — external signing mode', () => {
getKeyNock.isDone().should.equal(false);
});

it('should fall through to local path for non-UTXO coin in external mode', async () => {
coinStub = sinon.stub(coinFactory, 'getCoin').resolves(nonUtxoCoinStub);
it('should fall through to local path for unsupported coin in external mode', async () => {
coinStub = sinon.stub(coinFactory, 'getCoin').resolves(unsupportedCoinStub);

const signNock = nock(keyProviderUrl).post('/sign').reply(200, {});
nock(keyProviderUrl).get(`/key/${userPub}`).query({ source: 'user' }).reply(200, {
Expand All @@ -243,7 +241,7 @@ describe('signMultisigTransaction — external signing mode', () => {
});

await agent
.post(`/api/hteth/multisig/sign`)
.post(`/api/hxrp/multisig/sign`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ source: 'user', pub: userPub, txPrebuild: { txHex } });

Expand All @@ -261,4 +259,130 @@ describe('signMultisigTransaction — external signing mode', () => {

response.status.should.equal(500);
});

describe('ETH external signing', () => {
const mockOperationHash = '0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890';
const mockSignature =
'0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef12';
const mockExpireTime = 1735689600;

const ethCoinStub = {
getFamily: () => CoinFamily.ETH,
getFullName: () => 'Test Ethereum',
getDefaultExpireTime: sinon.stub().returns(mockExpireTime),
getOperationSha3ForExecuteAndConfirm: sinon.stub().returns(mockOperationHash),
} as unknown as BaseCoin;

const txPrebuild = {
recipients: [{ amount: '10000', address: '0xe9cbfdf9e02f4ee37ec81683a4be934b4eecc295' }],
nextContractSequenceId: 5,
gasLimit: 200000,
eip1559: { maxPriorityFeePerGas: '599413988', maxFeePerGas: '23556954878' },
isBatch: false,
};

it('should call POST /sign with operationHash and return halfSigned', async () => {
coinStub = sinon.stub(coinFactory, 'getCoin').resolves(ethCoinStub);

const signNock = nock(keyProviderUrl)
.post('/sign', {
pub: userPub,
source: 'user',
signablePayload: mockOperationHash,
algorithm: 'ecdsa',
})
.reply(200, { signature: mockSignature });

const getKeyNock = nock(keyProviderUrl).get(`/key/${userPub}`).reply(200, {});

const response = await agent
.post(`/api/hteth/multisig/sign`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ source: 'user', pub: userPub, txPrebuild });

response.status.should.equal(200);
response.body.should.have.property('halfSigned');
response.body.halfSigned.should.have.property('recipients');
response.body.halfSigned.should.have.property('expireTime', mockExpireTime);
response.body.halfSigned.should.have.property(
'contractSequenceId',
txPrebuild.nextContractSequenceId,
);
response.body.halfSigned.should.have.property('operationHash', mockOperationHash);
response.body.halfSigned.should.have.property('signature', mockSignature);
response.body.halfSigned.should.have.property('gasLimit', txPrebuild.gasLimit);
response.body.halfSigned.should.have.property('eip1559');
response.body.halfSigned.should.have.property('isBatch', txPrebuild.isBatch);

signNock.done();

/** Validate that the signing was done outside of the app: External Mode */
getKeyNock.isDone().should.equal(false);
});

it('should return error when recipients missing from txPrebuild', async () => {
coinStub = sinon.stub(coinFactory, 'getCoin').resolves(ethCoinStub);
const { recipients: __, ...txPrebuildWithoutRecipients } = txPrebuild;

const response = await agent
.post(`/api/hteth/multisig/sign`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ source: 'user', pub: userPub, txPrebuild: txPrebuildWithoutRecipients });

response.status.should.equal(500);
response.body.details.should.match(/recipients, nextContractSequenceId/);
});

it('should successfully sign when nextContractSequenceId is 0', async () => {
coinStub = sinon.stub(coinFactory, 'getCoin').resolves(ethCoinStub);

const txPrebuildWithZeroSequenceId = {
...txPrebuild,
nextContractSequenceId: 0,
};

const signNock = nock(keyProviderUrl)
.post('/sign', {
pub: userPub,
source: 'user',
signablePayload: mockOperationHash,
algorithm: 'ecdsa',
})
.reply(200, { signature: mockSignature });

const getKeyNock = nock(keyProviderUrl).get(`/key/${userPub}`).reply(200, {});

const response = await agent
.post(`/api/hteth/multisig/sign`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ source: 'user', pub: userPub, txPrebuild: txPrebuildWithZeroSequenceId });

response.status.should.equal(200);
response.body.should.have.property('halfSigned');
response.body.halfSigned.should.have.property('contractSequenceId', 0);

signNock.done();
getKeyNock.isDone().should.equal(false);
});

it('should return error when keyProvider sign fails', async () => {
coinStub = sinon.stub(coinFactory, 'getCoin').resolves(ethCoinStub);

nock(keyProviderUrl)
.post('/sign', {
pub: userPub,
source: 'user',
signablePayload: mockOperationHash,
algorithm: 'ecdsa',
})
.reply(500, { error: 'KMS unavailable' });

const response = await agent
.post(`/api/hteth/multisig/sign`)
.set('Authorization', `Bearer ${accessToken}`)
.send({ source: 'user', pub: userPub, txPrebuild });

response.status.should.equal(500);
});
});
});
Loading
Loading