diff --git a/modules/sdk-coin-algo/package.json b/modules/sdk-coin-algo/package.json index aaceb5ed30..1959fff607 100644 --- a/modules/sdk-coin-algo/package.json +++ b/modules/sdk-coin-algo/package.json @@ -42,7 +42,6 @@ "dependencies": { "@bitgo/sdk-core": "^25.1.0", "@bitgo/statics": "^46.1.0", - "@bitgo/utxo-lib": "^9.34.0", "@hashgraph/cryptography": "1.1.2", "@stablelib/hex": "^1.0.0", "algosdk": "1.14.0", diff --git a/modules/sdk-coin-algo/src/algo.ts b/modules/sdk-coin-algo/src/algo.ts index 06c28268dc..5e50e69e84 100644 --- a/modules/sdk-coin-algo/src/algo.ts +++ b/modules/sdk-coin-algo/src/algo.ts @@ -1,7 +1,6 @@ /** * @prettier */ -import * as utxolib from '@bitgo/utxo-lib'; import * as _ from 'lodash'; import { SeedValidator } from './seedValidator'; import { coins, CoinFamily } from '@bitgo/statics'; @@ -10,7 +9,6 @@ import { AddressCoinSpecific, BaseCoin, BitGoBase, - Ed25519KeyDeriver, InvalidAddressError, InvalidKey, KeyIndices, @@ -26,6 +24,7 @@ import { UnexpectedAddressError, VerifyAddressOptions, VerifyTransactionOptions, + NotSupported, } from '@bitgo/sdk-core'; import stellar from 'stellar-sdk'; @@ -169,12 +168,12 @@ export class Algo extends BaseCoin { return true; } - /** - * Generate ed25519 key pair - * - * @param seed - * @returns {Object} object with generated pub, prv - */ + /** inheritdoc */ + deriveKeyWithSeed(): { derivationPath: string; key: string } { + throw new NotSupported('method deriveKeyWithSeed not supported for eddsa curve'); + } + + /** inheritdoc */ generateKeyPair(seed?: Buffer): KeyPair { const keyPair = seed ? new AlgoLib.KeyPair({ seed }) : new AlgoLib.KeyPair(); const keys = keyPair.getKeys(); @@ -188,6 +187,16 @@ export class Algo extends BaseCoin { }; } + /** inheritdoc */ + generateRootKeyPair(seed?: Buffer): KeyPair { + const keyPair = seed ? new AlgoLib.KeyPair({ seed }) : new AlgoLib.KeyPair(); + const keys = keyPair.getKeys(); + if (!keys.prv) { + throw new Error('Missing prv in key generation.'); + } + return { prv: keys.prv + keys.pub, pub: keys.pub }; + } + /** * Return boolean indicating whether input is valid public key for the coin. * @@ -195,7 +204,7 @@ export class Algo extends BaseCoin { * @returns {Boolean} is it valid? */ isValidPub(pub: string): boolean { - return AlgoLib.algoUtils.isValidAddress(pub); + return AlgoLib.algoUtils.isValidAddress(pub) || AlgoLib.algoUtils.isValidPublicKey(pub); } /** @@ -207,7 +216,7 @@ export class Algo extends BaseCoin { * @returns {Boolean} is it valid? */ isValidPrv(prv: string): boolean { - return AlgoLib.algoUtils.isValidSeed(prv); + return AlgoLib.algoUtils.isValidSeed(prv) || AlgoLib.algoUtils.isValidPrivateKey(prv); } /** @@ -504,23 +513,6 @@ export class Algo extends BaseCoin { return true; } - /** @inheritDoc */ - deriveKeyWithSeed({ key, seed }: { key: string; seed: string }): { derivationPath: string; key: string } { - const derivationPathInput = utxolib.crypto.hash256(Buffer.from(seed, 'utf8')).toString('hex'); - const derivationPathParts = [ - 999999, - parseInt(derivationPathInput.slice(0, 7), 16), - parseInt(derivationPathInput.slice(7, 14), 16), - ]; - const derivationPath = 'm/' + derivationPathParts.map((part) => `${part}'`).join('/'); - const derivedKey = Ed25519KeyDeriver.derivePath(derivationPath, key).key; - const keypair = new AlgoLib.KeyPair({ seed: derivedKey }); - return { - key: keypair.getAddress(), - derivationPath, - }; - } - decodeTx(txn: Buffer): unknown { return AlgoLib.algoUtils.decodeAlgoTxn(txn); } diff --git a/modules/sdk-coin-algo/src/lib/transactionBuilder.ts b/modules/sdk-coin-algo/src/lib/transactionBuilder.ts index 5f9858bfaa..d5b43a21f6 100644 --- a/modules/sdk-coin-algo/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-algo/src/lib/transactionBuilder.ts @@ -14,9 +14,11 @@ import { BaseTransactionBuilder, BuildTransactionError, InvalidTransactionError, + isValidEd25519SecretKey, isValidEd25519Seed, TransactionType, } from '@bitgo/sdk-core'; +import { algoUtils } from '.'; const MIN_FEE = 1000; // in microalgos @@ -325,9 +327,18 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { /** @inheritdoc */ protected signImplementation({ key }: BaseKey): Transaction { - const buffKey = Utils.decodeSeed(key); - const keypair = new KeyPair({ prv: Buffer.from(buffKey.seed).toString('hex') }); - this._keyPairs.push(keypair); + try { + const buffKey = Utils.decodeSeed(key); + const keypair = new KeyPair({ prv: Buffer.from(buffKey.seed).toString('hex') }); + this._keyPairs.push(keypair); + } catch (e) { + if (algoUtils.isValidPrivateKey(key)) { + const keypair = new KeyPair({ prv: key }); + this._keyPairs.push(keypair); + } else { + throw e; + } + } return this._transaction; } @@ -371,6 +382,7 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { let isValidPrivateKeyFromBytes; const isValidPrivateKeyFromHex = isValidEd25519Seed(key); const isValidPrivateKeyFromBase64 = isValidEd25519Seed(Buffer.from(key, 'base64').toString('hex')); + const isValidRootPrvKey = isValidEd25519SecretKey(key); try { const decodedSeed = Utils.decodeSeed(key); isValidPrivateKeyFromBytes = isValidEd25519Seed(Buffer.from(decodedSeed.seed).toString('hex')); @@ -378,7 +390,12 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { isValidPrivateKeyFromBytes = false; } - if (!isValidPrivateKeyFromBytes && !isValidPrivateKeyFromHex && !isValidPrivateKeyFromBase64) { + if ( + !isValidPrivateKeyFromBytes && + !isValidPrivateKeyFromHex && + !isValidPrivateKeyFromBase64 && + !isValidRootPrvKey + ) { throw new BuildTransactionError(`Key validation failed`); } } diff --git a/modules/sdk-coin-algo/test/fixtures/algo.ts b/modules/sdk-coin-algo/test/fixtures/algo.ts index f5cd0b210b..0fe8e4bcb6 100644 --- a/modules/sdk-coin-algo/test/fixtures/algo.ts +++ b/modules/sdk-coin-algo/test/fixtures/algo.ts @@ -258,3 +258,17 @@ export const networks = { genesisHash: 'mFgazF+2uRS1tMiL9dsj01hJGySEmPN28B/TjjvpVW0=', }, } as const; + +export const rootKeyData = { + userKeyPair: { + prv: '7a35bb4007bc7cabad2119a264adf47d9148c484bb70759654d512054d814789de140f20b99f66356e57df3a727c7e5e58f9d5bd58a41c616ac183539b1d211a', + pub: 'de140f20b99f66356e57df3a727c7e5e58f9d5bd58a41c616ac183539b1d211a', + }, + backupPub: 'cb64e9729d5a1f1d669a678c8b4751584bd91d71646e27ede39eb92352cc4a34', + bitgoPub: 'JBL247YSR7BD4O3BSPK4NGENAEWKDGFYYMD73MFPJQ4GAW6B4XK4NKDAHU', + unsignedTx: + 'iaNhbXTOABFMhKNmZWXNA+iiZnbOAjfvgaNnZW6sdGVzdG5ldC12MS4womdoxCBIY7UYpLPITsgQ8i1PEIHLD3HwWaesIN7GL39w5Qk6IqJsds4CN/Npo3JjdsQgMP3MIxudjDYJr397seXvj5Y6SLByE1KN772IbZauSISjc25kxCBcMEptJu8de1vVNzOWxNZUtvnLrxmObmSa/brOdlWb6KR0eXBlo3BheQ==', + halfSignedTx: + 'gqRtc2lng6ZzdWJzaWeTgqJwa8Qg3hQPILmfZjVuV986cnx+Xlj51b1YpBxhasGDU5sdIRqhc8RAUSzKTZBm6OYGZqZ8KtSkIdwEDpOuFZR/O8saJfeC5E77UonmivLF5eQiAlE4+YD285IrXUSGvMgGu6s6wKyzBYGicGvEIMtk6XKdWh8dZppnjItHUVhL2R1xZG4n7eOeuSNSzEo0gaJwa8QgSFeufxKPwj47YZPVxpiNASyhmLjDB/2wr0w4YFvB5dWjdGhyAqF2AaN0eG6Jo2FtdM4AEUyEo2ZlZc0D6KJmds4CN++Bo2dlbqx0ZXN0bmV0LXYxLjCiZ2jEIEhjtRiks8hOyBDyLU8QgcsPcfBZp6wg3sYvf3DlCToiomx2zgI382mjcmN2xCAw/cwjG52MNgmvf3ux5e+PljpIsHITUo3vvYhtlq5IhKNzbmTEIFwwSm0m7x17W9U3M5bE1lS2+cuvGY5uZJr9us52VZvopHR5cGWjcGF5', + senderAddress: 'LQYEU3JG54OXWW6VG4ZZNRGWKS3PTS5PDGHG4ZE27W5M45SVTPUJDZENQA', +} as const; diff --git a/modules/sdk-coin-algo/test/unit/algo.ts b/modules/sdk-coin-algo/test/unit/algo.ts index da0307c1a1..a0cb3c95db 100644 --- a/modules/sdk-coin-algo/test/unit/algo.ts +++ b/modules/sdk-coin-algo/test/unit/algo.ts @@ -4,6 +4,7 @@ import { BitGoAPI } from '@bitgo/sdk-api'; import * as AlgoResources from '../fixtures/algo'; import { randomBytes } from 'crypto'; import { coins } from '@bitgo/statics'; +import { TransactionBuilderFactory } from '../../src/lib'; describe('ALGO:', function () { let bitgo: TestBitGoAPI; @@ -536,6 +537,20 @@ describe('ALGO:', function () { signed.txHex.should.equal(AlgoResources.rawTx.transfer.signed); }); + it('should sign transaction with root key', async function () { + const keypair = basecoin.generateRootKeyPair(AlgoResources.accounts.account1.secretKey); + + const signed = await basecoin.signTransaction({ + txPrebuild: { + txHex: AlgoResources.rawTx.transfer.unsigned, + keys: [keypair.pub], + addressVersion: 1, + }, + prv: keypair.prv, + }); + signed.txHex.should.equal(AlgoResources.rawTx.transfer.signed); + }); + it('should sign half signed transaction', async function () { const signed = await basecoin.signTransaction({ txPrebuild: { @@ -553,6 +568,29 @@ describe('ALGO:', function () { signed.txHex.should.equal(AlgoResources.rawTx.transfer.multisig); }); + it('should sign half signed transaction with root key', async function () { + const signed = await basecoin.signTransaction({ + txPrebuild: { + halfSigned: { + txHex: AlgoResources.rootKeyData.unsignedTx, + }, + keys: [ + AlgoResources.rootKeyData.userKeyPair.pub, + AlgoResources.rootKeyData.backupPub, + AlgoResources.rootKeyData.bitgoPub, + ], + addressVersion: 1, + }, + prv: AlgoResources.rootKeyData.userKeyPair.prv, + }); + + signed.txHex.should.deepEqual(AlgoResources.rootKeyData.halfSignedTx); + const factory = new TransactionBuilderFactory(coins.get('algo')); + const tx = await factory.from(signed.txHex).build(); + const txJson = tx.toJson(); + txJson.from.should.equal(AlgoResources.rootKeyData.senderAddress); + }); + it('should verify sign params if the key array contains addresses', function () { const keys = [ AlgoResources.accounts.account1.address, @@ -620,19 +658,24 @@ describe('ALGO:', function () { basecoin.isValidPub(kp.pub).should.equal(true); basecoin.isValidPrv(kp.prv).should.equal(true); }); + }); - it('should deterministically derive keypair with seed', () => { - const derivedKeypair = basecoin.deriveKeyWithSeed({ - key: 'UBI2KNGT742KGIPHMZDJHHSADIT56HRDPVOOCCRYIETD4BAJLCBMQNSCNE', - seed: 'cold derivation seed', - }); - console.log(JSON.stringify(derivedKeypair)); + describe('Generate wallet Root key pair: ', () => { + it('should generate key pair', () => { + const kp = basecoin.generateRootKeyPair(); + basecoin.isValidPub(kp.pub).should.equal(true); + basecoin.isValidPrv(kp.prv).should.equal(true); + }); - basecoin.isValidPub(derivedKeypair.key).should.be.true(); - derivedKeypair.should.deepEqual({ - key: 'NAYUBR4HQKJBBTNNKXQIY7GMHUCHVAUH5DQ4ZVHVL67CUQLGHRWDKQAHYU', - derivationPath: "m/999999'/25725073'/5121434'", - }); + it('should generate key pair from seed', () => { + const seed = Buffer.from('9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60', 'hex'); + const kp = basecoin.generateRootKeyPair(seed); + basecoin.isValidPub(kp.pub).should.equal(true); + kp.pub.should.equal('d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a'); + basecoin.isValidPrv(kp.prv).should.equal(true); + kp.prv.should.equal( + '9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a' + ); }); }); @@ -702,4 +745,12 @@ describe('ALGO:', function () { explain.operations[0].coin.should.equals('talgo:USDC-10458941'); }); }); + + describe('deriveKeyWithSeed', function () { + it('should derive key with seed', function () { + (() => { + basecoin.deriveKeyWithSeed('test'); + }).should.throw('method deriveKeyWithSeed not supported for eddsa curve'); + }); + }); });