PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-coin-algo/test/unit/lib/transactionBuilder
Просмотр файла: base.ts
import crypto from 'crypto';
import { BaseCoin as CoinConfig, coins } from '@bitgo/statics';
import algosdk from 'algosdk';
import assert from 'assert';
import should from 'should';
import sinon, { assert as SinonAssert } from 'sinon';
import {
AddressValidationError,
InsufficientFeeError,
KeyPair,
TransactionBuilder,
algoUtils,
} from '../../../../src/lib';
import { Transaction } from '../../../../src/lib/transaction';
import * as AlgoResources from '../../../fixtures/resources';
import { BaseKey, TransactionType } from '@bitgo/sdk-core';
import { EncodedTx } from '../../../../src/lib/ifaces';
const STANDARD_REQUIRED_NUMBER_OF_SIGNERS = 2;
class StubTransactionBuilder extends TransactionBuilder {
constructor(coinConfig: Readonly<CoinConfig>) {
super(coinConfig);
}
getFee(): number {
return this._fee;
}
getSender(): string {
return this._sender;
}
getGenesisHash(): string {
return this._genesisHash;
}
getGenesisId(): string {
return this._genesisId;
}
getFirstRound(): number {
return this._firstRound;
}
getLastRound(): number {
return this._lastRound;
}
getLease(): Uint8Array | undefined {
return this._lease;
}
getNote(): Uint8Array | undefined {
return this._note;
}
getReKeyTo(): string | undefined {
return this._reKeyTo;
}
getKeyPairs(): KeyPair[] {
return this._keyPairs;
}
getTransaction(): Transaction {
return this._transaction;
}
buildImplementation(): Promise<Transaction> {
return super.buildImplementation();
}
fromImplementation(rawTransaction: Uint8Array | string): Transaction {
return super.fromImplementation(rawTransaction);
}
signImplementation(key: BaseKey): Transaction {
return super.signImplementation(key);
}
getSuggestedParams(): algosdk.SuggestedParams {
return this.suggestedParams;
}
protected buildAlgoTxn(): algosdk.Transaction {
throw new Error('Method not implemented.');
}
protected get transactionType(): TransactionType {
throw new Error('Method not implemented.');
}
}
describe('Algo Transaction Builder', () => {
let txnBuilder: StubTransactionBuilder;
const {
accounts: { account1, account2, account3 },
networks: { testnet },
} = AlgoResources;
beforeEach(() => {
const config = coins.get('algo');
txnBuilder = new StubTransactionBuilder(config);
});
describe('setter validation', () => {
it('should validate fee is not lt 1000 microalgos if flat fee is set to true', () => {
txnBuilder.isFlatFee(true);
assert.throws(
() => txnBuilder.fee({ fee: '999' }),
(e: Error) => e.name === InsufficientFeeError.name
);
should.doesNotThrow(() => txnBuilder.fee({ fee: '1000' }));
});
it('should validate sender address is a valid algo address', () => {
const spy = sinon.spy(txnBuilder, 'validateAddress');
assert.throws(
() => txnBuilder.sender({ address: 'asdf' }),
(e: Error) => e.name === AddressValidationError.name
);
should.doesNotThrow(() => txnBuilder.sender({ address: account1.address }));
SinonAssert.calledTwice(spy);
});
it('should validate number of signers is not less than 0', () => {
assert.throws(() => txnBuilder.numberOfRequiredSigners(-1), /Number of signers: '-1' cannot be negative/);
for (let i = 0; i < STANDARD_REQUIRED_NUMBER_OF_SIGNERS; i++) {
should.doesNotThrow(() => txnBuilder.numberOfRequiredSigners(i));
}
});
});
describe('suggested params verification', () => {
it('should retrieve the suggested parameters as they have been set', () => {
const isFlatFee = true;
const fee = 1000;
const firstRound = 1;
const lastRound = 10;
const genesisId = 'testnet-v1.0';
const genesisHash = 'SGO1GKSzyE7IEPItTxCByw9x8FmnrCDexi9/cOUJOiI=';
txnBuilder
.isFlatFee(isFlatFee)
.fee({ fee: fee.toString() })
.firstRound(firstRound)
.lastRound(lastRound)
.genesisId(genesisId)
.genesisHash(genesisHash);
const suggestedParams = txnBuilder.getSuggestedParams();
should.equal(isFlatFee, suggestedParams.flatFee);
should.equal(fee, suggestedParams.fee);
should.equal(firstRound, suggestedParams.firstRound);
should.equal(lastRound, suggestedParams.lastRound);
should.equal(genesisId, suggestedParams.genesisID);
should.equal(genesisHash, suggestedParams.genesisHash);
});
});
describe('private key validation', () => {
it('validates byte arrays', () => {
should.doesNotThrow(() => txnBuilder.validateKey({ key: account1.secretKey }));
});
it('validates hex encoded strings', () => {
should.doesNotThrow(() => txnBuilder.validateKey({ key: account1.secretKey.toString('hex') }));
});
it('validates base64 encoded strings', () => {
should.doesNotThrow(() => txnBuilder.validateKey({ key: account1.secretKey.toString('base64') }));
});
});
describe('implementation functions', () => {
const to = account1.address;
const from = account2.address;
const reKeyTo = account3.address;
const amount = 1000;
const firstRound = 1;
const lastRound = 10;
const closeRemainderTo = account3.address;
// Uint8array conversion required because algosdk checks if the constructor
// is Uint8Array.
const lease = new Uint8Array(crypto.randomBytes(32));
const note = new Uint8Array(Buffer.from('note', 'utf-8'));
const fee = 1000;
const algoTxn = algosdk.makePaymentTxnWithSuggestedParams(
from,
to,
amount,
closeRemainderTo,
note,
{
fee,
flatFee: true,
firstRound,
lastRound,
genesisID: testnet.genesisID,
genesisHash: testnet.genesisHash,
},
reKeyTo
);
algoTxn.fee = fee;
algoTxn.flatFee = true;
algoTxn.addLease(lease);
it('should assign all decoded fields into transaction builder', () => {
txnBuilder.fromImplementation(algosdk.encodeUnsignedTransaction(algoTxn));
should(txnBuilder.getFee()).equal(fee);
should(txnBuilder.getSender()).equal(from);
should(txnBuilder.getGenesisHash()).equal(testnet.genesisHash);
should(txnBuilder.getGenesisId()).equal(testnet.genesisID);
should(txnBuilder.getFirstRound()).equal(firstRound);
should(txnBuilder.getLastRound()).equal(lastRound);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
should(Buffer.from(txnBuilder.getLease()!).toString('hex')).equal(Buffer.from(lease).toString('hex'));
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
should(Buffer.from(txnBuilder.getNote()!).toString('hex')).equal(Buffer.from(note).toString('hex'));
should(txnBuilder.getReKeyTo()).equal(reKeyTo);
should(txnBuilder.getTransaction().getAlgoTransaction()).not.be.undefined();
});
it('should assign signers into transaction builder', () => {
sinon.stub(algoUtils, 'decodeAlgoTxn').callsFake((txnBytes: Uint8Array | string): EncodedTx => {
return {
rawTransaction: Buffer.from([]),
signed: true,
signers: [
'25NJQAMCWEFLPVKL73J4SZAHHIHOC4XT3KTCGJNPAINGR5YHKENMEF5QTE',
'7RI43DTCDCQ2HDNEP3IAEMHQLAEBN3ITXIZQHLC55OIRKSEQQAS52OYKJE',
],
txn: algoTxn,
};
});
txnBuilder.fromImplementation(algosdk.encodeUnsignedTransaction(algoTxn));
should(txnBuilder.getTransaction().signers).deepEqual([
'25NJQAMCWEFLPVKL73J4SZAHHIHOC4XT3KTCGJNPAINGR5YHKENMEF5QTE',
'7RI43DTCDCQ2HDNEP3IAEMHQLAEBN3ITXIZQHLC55OIRKSEQQAS52OYKJE',
]);
sinon.restore();
});
it('transaction builder should fail when signers are incorrect', () => {
sinon.stub(algoUtils, 'decodeAlgoTxn').callsFake((txnBytes: Uint8Array | string): EncodedTx => {
return {
rawTransaction: Buffer.from([]),
signed: true,
signers: [
'25NJQAMCWEFLPVKL73J4SZAHHIHOC4XT3KTCGJNPAINGR5YHKENMEF5AAA',
'7RI43DTCDCQ2HDNEP3IAEMHQLAEBN3ITXIZQHLC55OIRKSEQQAS52OYKJE',
],
txn: algoTxn,
};
});
should(() => txnBuilder.fromImplementation(algosdk.encodeUnsignedTransaction(algoTxn))).throw(
"The address '25NJQAMCWEFLPVKL73J4SZAHHIHOC4XT3KTCGJNPAINGR5YHKENMEF5AAA' is not a well-formed algorand address"
);
sinon.restore();
});
it('should not sign the transaction', () => {
const txn = txnBuilder.getTransaction();
txn.setAlgoTransaction(algoTxn);
txn.setNumberOfRequiredSigners(1);
assert.throws(
() => txnBuilder.signImplementation({ key: Buffer.from(account1.prvKey).toString('hex') }),
new RegExp('Invalid base32 characters')
);
});
it('should sign the transaction', () => {
const { prv, pub } = new KeyPair().recordKeysFromPrivateKeyInProtocolFormat(account1.prvKey);
const prvKey = algoUtils.encodeSeed(Buffer.from(prv + pub));
const txn = txnBuilder.getTransaction();
txn.setAlgoTransaction(algoTxn);
txn.setNumberOfRequiredSigners(1);
should.doesNotThrow(() => txnBuilder.signImplementation({ key: prvKey }));
});
});
describe('transaction validation', () => {
it('should validate a normal transaction', () => {
txnBuilder
.fee({ fee: '1000' })
.isFlatFee(true)
.firstRound(1)
.lastRound(10)
.sender({ address: account1.address })
.genesisId(testnet.genesisID)
.genesisHash(testnet.genesisHash);
should.doesNotThrow(() => txnBuilder.validateTransaction(txnBuilder.getTransaction()));
});
it('should validate last round is after first round', () => {
txnBuilder
.fee({ fee: '1000' })
.isFlatFee(true)
.firstRound(10)
.lastRound(1)
.sender({ address: account1.address })
.genesisId(testnet.genesisID)
.genesisHash(testnet.genesisHash);
assert.throws(
() => txnBuilder.validateTransaction(txnBuilder.getTransaction()),
new RegExp(
'Transaction validation failed: "value" failed custom validation because lastRound cannot be greater than or equal to firstRound'
)
);
});
it('should build a normal transaction with correct signers', () => {
txnBuilder
.fee({ fee: '1000' })
.isFlatFee(true)
.firstRound(1)
.lastRound(10)
.sender({ address: account1.address })
.setSigners([
'25NJQAMCWEFLPVKL73J4SZAHHIHOC4XT3KTCGJNPAINGR5YHKENMEF5QTE',
'7RI43DTCDCQ2HDNEP3IAEMHQLAEBN3ITXIZQHLC55OIRKSEQQAS52OYKJE',
])
.genesisId(testnet.genesisID)
.genesisHash(testnet.genesisHash);
should(txnBuilder.getTransaction().signers).deepEqual([
'25NJQAMCWEFLPVKL73J4SZAHHIHOC4XT3KTCGJNPAINGR5YHKENMEF5QTE',
'7RI43DTCDCQ2HDNEP3IAEMHQLAEBN3ITXIZQHLC55OIRKSEQQAS52OYKJE',
]);
});
it('should not build a normal transaction with incorrect signers', () => {
should(() =>
txnBuilder
.fee({ fee: '1000' })
.isFlatFee(true)
.firstRound(1)
.lastRound(10)
.sender({ address: account1.address })
.setSigners([
'25NJQAMCWEFLPVKL73J4SZAHHIHOC4XT3KTCGJNPAINGR5YHKENMEF5AAA',
'7RI43DTCDCQ2HDNEP3IAEMHQLAEBN3ITXIZQHLC55OIRKSEQQAS52OYKJE',
])
.genesisId(testnet.genesisID)
.genesisHash(testnet.genesisHash)
).throw(
"The address '25NJQAMCWEFLPVKL73J4SZAHHIHOC4XT3KTCGJNPAINGR5YHKENMEF5AAA' is not a well-formed algorand address"
);
});
});
});
Выполнить команду
Для локальной разработки. Не используйте в интернете!