diff --git a/CHANGELOG.md b/CHANGELOG.md
index e0b1167a1..d0059da0a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+# v1.19.1
+
+### Enhancements
+
+- API: Support attaching signatures to standard and multisig transactions by @jdtzmn in https://github.com/algorand/js-algorand-sdk/pull/595
+- AVM: Consolidate TEAL and AVM versions by @michaeldiamant in https://github.com/algorand/js-algorand-sdk/pull/609
+- Testing: Use Dev mode network for cucumber tests by @algochoi in https://github.com/algorand/js-algorand-sdk/pull/614
+
# v1.19.0
## What's Changed
diff --git a/Makefile b/Makefile
index c0dbe8307..f41f6790c 100644
--- a/Makefile
+++ b/Makefile
@@ -2,7 +2,7 @@ unit:
node_modules/.bin/cucumber-js --tags "@unit.offline or @unit.algod or @unit.indexer or @unit.rekey or @unit.tealsign or @unit.dryrun or @unit.applications or @unit.responses or @unit.transactions or @unit.transactions.keyreg or @unit.transactions.payment or @unit.responses.231 or @unit.feetest or @unit.indexer.logs or @unit.abijson or @unit.abijson.byname or @unit.atomic_transaction_composer or @unit.responses.unlimited_assets or @unit.indexer.ledger_refactoring or @unit.algod.ledger_refactoring or @unit.dryrun.trace.application or @unit.sourcemap" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
integration:
- node_modules/.bin/cucumber-js --tags "@algod or @assets or @auction or @kmd or @send or @indexer or @rekey or @send.keyregtxn or @dryrun or @compile or @applications or @indexer.applications or @applications.verified or @indexer.231 or @abi or @c2c or @compile.sourcemap" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
+ node_modules/.bin/cucumber-js --tags "@algod or @assets or @auction or @kmd or @send or @indexer or @rekey_v1 or @send.keyregtxn or @dryrun or @compile or @applications or @indexer.applications or @applications.verified or @indexer.231 or @abi or @c2c or @compile.sourcemap" tests/cucumber/features --require-module ts-node/register --require tests/cucumber/steps/index.js
docker-test:
./tests/cucumber/docker/run_docker.sh
diff --git a/README.md b/README.md
index d28deac83..24bd04c7a 100644
--- a/README.md
+++ b/README.md
@@ -22,8 +22,8 @@ Include a minified browser bundle directly in your HTML like so:
```html
```
@@ -32,8 +32,8 @@ or
```html
```
diff --git a/package-lock.json b/package-lock.json
index 9773bb7a5..7b6856a2a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "algosdk",
- "version": "1.19.0",
+ "version": "1.19.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "algosdk",
- "version": "1.19.0",
+ "version": "1.19.1",
"license": "MIT",
"dependencies": {
"algo-msgpack-with-bigint": "^2.1.1",
diff --git a/package.json b/package.json
index 9648348b2..7a3234197 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "algosdk",
- "version": "1.19.0",
+ "version": "1.19.1",
"description": "The official JavaScript SDK for Algorand",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
diff --git a/src/logic/logic.ts b/src/logic/logic.ts
index 9d8d60ef6..9f66b23f6 100644
--- a/src/logic/logic.ts
+++ b/src/logic/logic.ts
@@ -247,9 +247,7 @@ export function readProgram(
}
// costs calculated dynamically starting in v4
if (version < 4 && cost > maxCost) {
- throw new Error(
- 'program too costly for Teal version < 4. consider using v4.'
- );
+ throw new Error('program too costly for version < 4. consider using v4.');
}
return [ints, byteArrays, true];
}
diff --git a/src/main.ts b/src/main.ts
index 12d853485..9c27efa82 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -166,6 +166,9 @@ export {
signMultisigTransaction,
mergeMultisigTransactions,
appendSignMultisigTransaction,
+ createMultisigTransaction,
+ appendSignRawMultisigSignature,
+ verifyMultisig,
multisigAddress,
} from './multisig';
export { SourceMap } from './logic/sourcemap';
diff --git a/src/multisig.ts b/src/multisig.ts
index 5c89fcb1b..245ac6749 100644
--- a/src/multisig.ts
+++ b/src/multisig.ts
@@ -29,6 +29,8 @@ export const MULTISIG_NO_MUTATE_ERROR_MSG =
'Cannot mutate a multisig field as it would invalidate all existing signatures.';
export const MULTISIG_USE_PARTIAL_SIGN_ERROR_MSG =
'Cannot sign a multisig transaction using `signTxn`. Use `partialSignTxn` instead.';
+export const MULTISIG_SIGNATURE_LENGTH_ERROR_MSG =
+ 'Cannot add multisig signature. Signature is not of the correct length.';
interface MultisigOptions {
rawSig: Uint8Array;
@@ -40,41 +42,27 @@ interface MultisigMetadataWithPks extends Omit {
}
/**
- * createMultisigTransaction creates a multisig transaction blob.
- * @param txnForEncoding - the actual transaction to sign.
- * @param rawSig - a Buffer raw signature of that transaction
- * @param myPk - a public key that corresponds with rawSig
+ * createRawMultisigTransaction creates a raw, unsigned multisig transaction blob.
+ * @param txn - the actual transaction.
* @param version - multisig version
- * @param threshold - mutlisig threshold
+ * @param threshold - multisig threshold
* @param pks - ordered list of public keys in this multisig
* @returns encoded multisig blob
*/
-function createMultisigTransaction(
- txnForEncoding: EncodedTransaction,
- { rawSig, myPk }: MultisigOptions,
- { version, threshold, pks }: MultisigMetadataWithPks
+export function createMultisigTransaction(
+ txn: txnBuilder.Transaction,
+ { version, threshold, addrs }: MultisigMetadata
) {
- let keyExist = false;
// construct the appendable multisigned transaction format
- const subsigs = pks.map((pk) => {
- if (nacl.bytesEqual(pk, myPk)) {
- keyExist = true;
- return {
- pk: Buffer.from(pk),
- s: rawSig,
- };
- }
- return { pk: Buffer.from(pk) };
- });
- if (keyExist === false) {
- throw new Error(MULTISIG_KEY_NOT_EXIST_ERROR_MSG);
- }
+ const pks = addrs.map((addr) => address.decodeAddress(addr).publicKey);
+ const subsigs = pks.map((pk) => ({ pk: Buffer.from(pk) }));
const msig: EncodedMultisig = {
v: version,
thr: threshold,
subsig: subsigs,
};
+ const txnForEncoding = txn.get_obj_for_encoding();
const signedTxn: EncodedSignedTransaction = {
msig,
txn: txnForEncoding,
@@ -97,6 +85,58 @@ function createMultisigTransaction(
return new Uint8Array(encoding.encode(signedTxn));
}
+/**
+ * createMultisigTransactionWithSignature creates a multisig transaction blob with an included signature.
+ * @param txn - the actual transaction to sign.
+ * @param rawSig - a Buffer raw signature of that transaction
+ * @param myPk - a public key that corresponds with rawSig
+ * @param version - multisig version
+ * @param threshold - multisig threshold
+ * @param pks - ordered list of public keys in this multisig
+ * @returns encoded multisig blob
+ */
+function createMultisigTransactionWithSignature(
+ txn: txnBuilder.Transaction,
+ { rawSig, myPk }: MultisigOptions,
+ { version, threshold, pks }: MultisigMetadataWithPks
+) {
+ // Create an empty encoded multisig transaction
+ const encodedMsig = createMultisigTransaction(txn, {
+ version,
+ threshold,
+ addrs: pks.map((pk) => address.encodeAddress(pk)),
+ });
+ // note: this is not signed yet, but will be shortly
+ const signedTxn = encoding.decode(encodedMsig) as EncodedSignedTransaction;
+
+ let keyExist = false;
+ // append the multisig signature to the corresponding public key in the multisig blob
+ signedTxn.msig.subsig.forEach((subsig, i) => {
+ if (nacl.bytesEqual(subsig.pk, myPk)) {
+ keyExist = true;
+ signedTxn.msig.subsig[i].s = rawSig;
+ }
+ });
+ if (keyExist === false) {
+ throw new Error(MULTISIG_KEY_NOT_EXIST_ERROR_MSG);
+ }
+
+ // if the address of this multisig is different from the transaction sender,
+ // we need to add the auth-addr field
+ const msigAddr = address.fromMultisigPreImg({
+ version,
+ threshold,
+ pks,
+ });
+ if (
+ address.encodeAddress(signedTxn.txn.snd) !== address.encodeAddress(msigAddr)
+ ) {
+ signedTxn.sgnr = Buffer.from(msigAddr);
+ }
+
+ return new Uint8Array(encoding.encode(signedTxn));
+}
+
/**
* MultisigTransaction is a Transaction that also supports creating partially-signed multisig transactions.
*/
@@ -140,13 +180,39 @@ export class MultisigTransaction extends txnBuilder.Transaction {
) {
// get signature verifier
const myPk = nacl.keyPairFromSecretKey(sk).publicKey;
- return createMultisigTransaction(
- this.get_obj_for_encoding(),
+ return createMultisigTransactionWithSignature(
+ this,
{ rawSig: this.rawSignTxn(sk), myPk },
{ version, threshold, pks }
);
}
+ /**
+ * partialSignWithMultisigSignature partially signs this transaction with an external raw multisig signature and returns
+ * a partially-signed multisig transaction, encoded with msgpack as a typed array.
+ * @param metadata - multisig metadata
+ * @param signerAddr - address of the signer
+ * @param signature - raw multisig signature
+ * @returns an encoded, partially signed multisig transaction.
+ */
+ partialSignWithMultisigSignature(
+ metadata: MultisigMetadataWithPks,
+ signerAddr: string,
+ signature: Uint8Array
+ ) {
+ if (!nacl.isValidSignatureLength(signature.length)) {
+ throw new Error(MULTISIG_SIGNATURE_LENGTH_ERROR_MSG);
+ }
+ return createMultisigTransactionWithSignature(
+ this,
+ {
+ rawSig: signature,
+ myPk: address.decodeAddress(signerAddr).publicKey,
+ },
+ metadata
+ );
+ }
+
// eslint-disable-next-line camelcase
static from_obj_for_encoding(
txnForEnc: EncodedTransaction
@@ -312,7 +378,7 @@ export function verifyMultisig(
/**
* signMultisigTransaction takes a raw transaction (see signTransaction), a multisig preimage, a secret key, and returns
* a multisig transaction, which is a blob representing a transaction and multisignature account preimage. The returned
- * multisig txn can accumulate additional signatures through mergeMultisigTransactions or appendMultisigTransaction.
+ * multisig txn can accumulate additional signatures through mergeMultisigTransactions or appendSignMultisigTransaction.
* @param txn - object with either payment or key registration fields
* @param version - multisig version
* @param threshold - multisig threshold
@@ -391,9 +457,43 @@ export function appendSignMultisigTransaction(
};
}
+/**
+ * appendMultisigTransactionSignature takes a multisig transaction blob, and appends a given raw signature to it.
+ * This makes it possible to compile a multisig signature using only raw signatures from external methods.
+ * @param multisigTxnBlob - an encoded multisig txn. Supports non-payment txn types.
+ * @param version - multisig version
+ * @param threshold - multisig threshold
+ * @param addrs - a list of Algorand addresses representing possible signers for this multisig. Order is important.
+ * @param signerAddr - address of the signer
+ * @param signature - raw multisig signature
+ * @returns object containing txID, and blob representing encoded multisig txn
+ */
+export function appendSignRawMultisigSignature(
+ multisigTxnBlob: Uint8Array,
+ { version, threshold, addrs }: MultisigMetadata,
+ signerAddr: string,
+ signature: Uint8Array
+) {
+ const pks = addrs.map((addr) => address.decodeAddress(addr).publicKey);
+ // obtain underlying txn, sign it, and merge it
+ const multisigTxObj = encoding.decode(
+ multisigTxnBlob
+ ) as EncodedSignedTransaction;
+ const msigTxn = MultisigTransaction.from_obj_for_encoding(multisigTxObj.txn);
+ const partialSignedBlob = msigTxn.partialSignWithMultisigSignature(
+ { version, threshold, pks },
+ signerAddr,
+ signature
+ );
+ return {
+ txID: msigTxn.txID().toString(),
+ blob: mergeMultisigTransactions([multisigTxnBlob, partialSignedBlob]),
+ };
+}
+
/**
* multisigAddress takes multisig metadata (preimage) and returns the corresponding human readable Algorand address.
- * @param version - mutlisig version
+ * @param version - multisig version
* @param threshold - multisig threshold
* @param addrs - list of Algorand addresses
*/
diff --git a/src/nacl/naclWrappers.ts b/src/nacl/naclWrappers.ts
index 754f48ff2..91027ee0b 100644
--- a/src/nacl/naclWrappers.ts
+++ b/src/nacl/naclWrappers.ts
@@ -18,6 +18,10 @@ export function keyPair() {
return keyPairFromSeed(seed);
}
+export function isValidSignatureLength(len: number) {
+ return len === nacl.sign.signatureLength;
+}
+
export function keyPairFromSecretKey(sk: Uint8Array) {
return nacl.sign.keyPair.fromSecretKey(sk);
}
diff --git a/src/transaction.ts b/src/transaction.ts
index 10825a43e..4f737a037 100644
--- a/src/transaction.ts
+++ b/src/transaction.ts
@@ -1035,6 +1035,22 @@ export class Transaction implements TransactionStorageStructure {
return new Uint8Array(encoding.encode(sTxn));
}
+ attachSignature(signerAddr: string, signature: Uint8Array) {
+ if (!nacl.isValidSignatureLength(signature.length)) {
+ throw new Error('Invalid signature length');
+ }
+ const sTxn: EncodedSignedTransaction = {
+ sig: Buffer.from(signature),
+ txn: this.get_obj_for_encoding(),
+ };
+ // add AuthAddr if signing with a different key than From indicates
+ if (signerAddr !== address.encodeAddress(this.from.publicKey)) {
+ const signerPublicKey = address.decodeAddress(signerAddr).publicKey;
+ sTxn.sgnr = Buffer.from(signerPublicKey);
+ }
+ return new Uint8Array(encoding.encode(sTxn));
+ }
+
rawTxID() {
const enMsg = this.toByte();
const gh = Buffer.from(utils.concatArrays(this.tag, enMsg));
diff --git a/tests/4.Utils.ts b/tests/4.Utils.ts
index a06abbb61..c372fed91 100644
--- a/tests/4.Utils.ts
+++ b/tests/4.Utils.ts
@@ -1,5 +1,6 @@
import assert from 'assert';
import * as utils from '../src/utils/utils';
+import * as nacl from '../src/nacl/naclWrappers';
describe('utils', () => {
describe('concatArrays', () => {
@@ -33,3 +34,10 @@ describe('utils', () => {
});
});
});
+
+describe('nacl wrapper', () => {
+ it('should validate signature length', () => {
+ assert.strictEqual(nacl.isValidSignatureLength(6), false);
+ assert.strictEqual(nacl.isValidSignatureLength(64), true);
+ });
+});
diff --git a/tests/6.Multisig.ts b/tests/6.Multisig.ts
index d4b0e2475..7dc759608 100644
--- a/tests/6.Multisig.ts
+++ b/tests/6.Multisig.ts
@@ -4,6 +4,7 @@ import {
MultisigTransaction,
MULTISIG_NO_MUTATE_ERROR_MSG,
MULTISIG_USE_PARTIAL_SIGN_ERROR_MSG,
+ MULTISIG_SIGNATURE_LENGTH_ERROR_MSG,
} from '../src/multisig';
const sampleAccount1 = algosdk.mnemonicToSecretKey(
@@ -167,6 +168,169 @@ describe('Multisig Functionality', () => {
});
});
+ describe('create/append multisig with external signatures', () => {
+ it('should match golden main repo result', () => {
+ const oneSigTxn = Buffer.from(
+ 'gqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RAuLAFE0oma0skOoAmOzEwfPuLYpEWl4LINtsiLrUqWQkDxh4WHb29//YCpj4MFbiSgD2jKYt0XKRD86zKCF4RDYGicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxgaJwa8Qg5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKGjdGhyAqF2AaN0eG6Lo2FtdM0D6KVjbG9zZcQgQOk0koglZMvOnFmmm2dUJonpocOiqepbZabopEIf/FejZmVlzQPoomZ2zfMVo2dlbqxkZXZuZXQtdjM4LjCiZ2jEIP6zbDkQFDkAw9pVQsoYNrAP0vgZWRJXzSP2BC+YyDadomx2zfb9pG5vdGXECEUmIgAYUob7o3JjdsQge2ziT+tbrMCxZOKcIixX9fY9w4fUOQSCWEEcX+EPfAKjc25kxCCNkrSJkAFzoE36Q1mjZmpq/OosQqBd2cH3PuulR4A36aR0eXBlo3BheQ==',
+ 'base64'
+ );
+
+ const signerAddr = sampleAccount2.addr;
+ const signedTxn = algosdk.appendSignMultisigTransaction(
+ oneSigTxn,
+ sampleMultisigParams,
+ sampleAccount2.sk
+ );
+
+ const multisig = algosdk.decodeSignedTransaction(signedTxn.blob).msig;
+ if (multisig === undefined) {
+ throw new Error('multisig is undefined');
+ }
+
+ const signatures = multisig.subsig;
+ if (signatures === undefined) {
+ throw new Error('No signatures found');
+ }
+
+ const signature = signatures[1].s;
+ if (signature === undefined) {
+ throw new Error('No signature found');
+ }
+
+ const { txID, blob } = algosdk.appendSignRawMultisigSignature(
+ oneSigTxn,
+ sampleMultisigParams,
+ signerAddr,
+ signature
+ );
+
+ const expectedTxID =
+ 'MANN3ESOHQVHFZBAGD6UK6XFVWEFZQJPWO5SQ2J5LZRCF5E2VVQQ';
+ assert.strictEqual(txID, expectedTxID);
+
+ const expectedBlob = Buffer.from(
+ 'gqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RAuLAFE0oma0skOoAmOzEwfPuLYpEWl4LINtsiLrUqWQkDxh4WHb29//YCpj4MFbiSgD2jKYt0XKRD86zKCF4RDYKicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxoXPEQBAhuyRjsOrnHp3s/xI+iMKiL7QPsh8iJZ22YOJJP0aFUwedMr+a6wfdBXk1OefyrAN1wqJ9rq6O+DrWV1fH0ASBonBrxCDn8PhNBoEd+fMcjYeLEVX0Zx1RoYXCAJCGZ/RJWHBooaN0aHICoXYBo3R4boujYW10zQPopWNsb3NlxCBA6TSSiCVky86cWaabZ1Qmiemhw6Kp6ltlpuikQh/8V6NmZWXNA+iiZnbN8xWjZ2VurGRldm5ldC12MzguMKJnaMQg/rNsORAUOQDD2lVCyhg2sA/S+BlZElfNI/YEL5jINp2ibHbN9v2kbm90ZcQIRSYiABhShvujcmN2xCB7bOJP61uswLFk4pwiLFf19j3Dh9Q5BIJYQRxf4Q98AqNzbmTEII2StImQAXOgTfpDWaNmamr86ixCoF3Zwfc+66VHgDfppHR5cGWjcGF5',
+ 'base64'
+ );
+
+ assert.deepStrictEqual(Buffer.from(blob), expectedBlob);
+ });
+
+ it('should not sign with signature of invalid length', () => {
+ const oneSigTxn = Buffer.from(
+ 'gqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RAuLAFE0oma0skOoAmOzEwfPuLYpEWl4LINtsiLrUqWQkDxh4WHb29//YCpj4MFbiSgD2jKYt0XKRD86zKCF4RDYGicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxgaJwa8Qg5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKGjdGhyAqF2AaN0eG6Lo2FtdM0D6KVjbG9zZcQgQOk0koglZMvOnFmmm2dUJonpocOiqepbZabopEIf/FejZmVlzQPoomZ2zfMVo2dlbqxkZXZuZXQtdjM4LjCiZ2jEIP6zbDkQFDkAw9pVQsoYNrAP0vgZWRJXzSP2BC+YyDadomx2zfb9pG5vdGXECEUmIgAYUob7o3JjdsQge2ziT+tbrMCxZOKcIixX9fY9w4fUOQSCWEEcX+EPfAKjc25kxCCNkrSJkAFzoE36Q1mjZmpq/OosQqBd2cH3PuulR4A36aR0eXBlo3BheQ==',
+ 'base64'
+ );
+
+ const signerAddr = sampleAccount2.addr;
+ const signedTxn = algosdk.appendSignMultisigTransaction(
+ oneSigTxn,
+ sampleMultisigParams,
+ sampleAccount2.sk
+ );
+
+ const multisig = algosdk.decodeSignedTransaction(signedTxn.blob).msig;
+ if (multisig === undefined) {
+ throw new Error('multisig is undefined');
+ }
+
+ const signatures = multisig.subsig;
+ if (signatures === undefined) {
+ throw new Error('No signatures found');
+ }
+
+ const signature = signatures[1].s;
+ if (signature === undefined) {
+ throw new Error('No signature found');
+ }
+
+ // Remove the last byte of the signature
+ const invalidSignature = signature.slice(0, -1);
+ assert.throws(
+ () =>
+ algosdk.appendSignRawMultisigSignature(
+ oneSigTxn,
+ sampleMultisigParams,
+ signerAddr,
+ invalidSignature
+ ),
+ Error(MULTISIG_SIGNATURE_LENGTH_ERROR_MSG)
+ );
+ });
+
+ it('should append signature to created raw multisig transaction', () => {
+ const rawTxBlob = Buffer.from(
+ 'jKNmZWXOAAPIwKJmds4ADvnao2dlbqxkZXZuZXQtdjM4LjCiZ2jEIP6zbDkQFDkAw9pVQsoYNrAP0vgZWRJXzSP2BC+YyDadomx2zgAO/cKmc2Vsa2V5xCAyEisr1j3cUzGWF6WqU8Sxwm/j3MryjTYitWl3oUBchqNzbmTEII2StImQAXOgTfpDWaNmamr86ixCoF3Zwfc+66VHgDfppHR5cGWma2V5cmVnp3ZvdGVmc3TOAA27oKZ2b3Rla2TNJxCndm90ZWtlecQgcBvX+5ErB7MIEf8oHZ/ulWPlgC4gJokjGSWPd/qTHoindm90ZWxzdM4AD0JA',
+ 'base64'
+ );
+ const decRawTx = algosdk.decodeUnsignedTransaction(rawTxBlob);
+
+ const unsignedMultisigTx = algosdk.createMultisigTransaction(
+ decRawTx,
+ sampleMultisigParams
+ );
+
+ // Check that the unsignedMultisigTx is valid
+ interface ExpectedMultisigTxStructure {
+ msig: {
+ subsig: {
+ pk: Uint8Array;
+ s: Uint8Array;
+ }[];
+ };
+ }
+ const unsignedMultisigTxBlob = algosdk.decodeObj(
+ unsignedMultisigTx
+ ) as ExpectedMultisigTxStructure;
+ assert.deepStrictEqual(
+ unsignedMultisigTxBlob.msig.subsig[0].pk,
+ algosdk.decodeAddress(sampleAccount1.addr).publicKey
+ );
+ assert.strictEqual(unsignedMultisigTxBlob.msig.subsig[1].s, undefined);
+
+ // Sign the raw transaction with a signature generated from the first account
+ const signerAddr = sampleAccount1.addr;
+ const signedTxn = algosdk.appendSignMultisigTransaction(
+ unsignedMultisigTx,
+ sampleMultisigParams,
+ sampleAccount1.sk
+ );
+
+ const multisig = algosdk.decodeSignedTransaction(signedTxn.blob).msig;
+ if (multisig === undefined) {
+ throw new Error('multisig is undefined');
+ }
+
+ const signatures = multisig.subsig;
+ if (signatures === undefined) {
+ throw new Error('No signatures found');
+ }
+
+ const signature = signatures[0].s;
+ if (signature === undefined) {
+ throw new Error('No signature found');
+ }
+ const { txID, blob } = algosdk.appendSignRawMultisigSignature(
+ unsignedMultisigTx,
+ sampleMultisigParams,
+ signerAddr,
+ signature
+ );
+
+ // Check that the signed raw multisig is valid
+ const expectedTxID =
+ 'E7DA7WTJCWWFQMKSVU5HOIJ5F5HGVGMOZGBIHRJRYGIX7FIJ5VWA';
+ assert.strictEqual(txID, expectedTxID);
+
+ const expectedBlob = Buffer.from(
+ 'gqRtc2lng6ZzdWJzaWeTgqJwa8QgG37AsEvqYbeWkJfmy/QH4QinBTUdC8mKvrEiCairgXihc8RAcT0s17wJbvnza+NpyHwM0RWbQ+HwKmsT1PLs+w6d6MpdTH3tra+yKZE0K0qEyhSE7Y56+B9oaf2orEbjc/njDYGicGvEIAljMglTc4nwdWcRdzmRx9A+G3PIxPUr9q/wGqJc+cJxgaJwa8Qg5/D4TQaBHfnzHI2HixFV9GcdUaGFwgCQhmf0SVhwaKGjdGhyAqF2AaN0eG6Mo2ZlZc4AA8jAomZ2zgAO+dqjZ2VurGRldm5ldC12MzguMKJnaMQg/rNsORAUOQDD2lVCyhg2sA/S+BlZElfNI/YEL5jINp2ibHbOAA79wqZzZWxrZXnEIDISKyvWPdxTMZYXpapTxLHCb+PcyvKNNiK1aXehQFyGo3NuZMQgjZK0iZABc6BN+kNZo2ZqavzqLEKgXdnB9z7rpUeAN+mkdHlwZaZrZXlyZWendm90ZWZzdM4ADbugpnZvdGVrZM0nEKd2b3Rla2V5xCBwG9f7kSsHswgR/ygdn+6VY+WALiAmiSMZJY93+pMeiKd2b3RlbHN0zgAPQkA=',
+ 'base64'
+ );
+
+ assert.deepStrictEqual(Buffer.from(blob), expectedBlob);
+ });
+ });
+
describe('should sign keyreg transaction types', () => {
it('first partial sig should match golden main repo result', () => {
const rawTxBlob = Buffer.from(
diff --git a/tests/7.AlgoSDK.js b/tests/7.AlgoSDK.js
index 6ad333d09..e831786b9 100644
--- a/tests/7.AlgoSDK.js
+++ b/tests/7.AlgoSDK.js
@@ -261,6 +261,7 @@ describe('Algosdk (AKA end to end)', () => {
const signed = algosdk.signBytes(toSign, account.sk);
assert.equal(true, algosdk.verifyBytes(toSign, signed, account.addr));
});
+
it('should not verify a corrupted signature', () => {
const account = algosdk.generateAccount();
const toSign = Buffer.from([1, 9, 25, 49]);
@@ -268,6 +269,71 @@ describe('Algosdk (AKA end to end)', () => {
signed[0] = (signed[0] + 1) % 256;
assert.equal(false, algosdk.verifyBytes(toSign, signed, account.addr));
});
+
+ it('should attach arbitrary signatures', () => {
+ const sender = algosdk.generateAccount();
+ const signer = algosdk.generateAccount();
+
+ // Create a transaction
+ const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
+ from: sender.addr,
+ to: signer.addr,
+ amount: 1000,
+ suggestedParams: {
+ firstRound: 12466,
+ lastRound: 13466,
+ genesisID: 'devnet-v33.0',
+ genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=',
+ fee: 4,
+ },
+ });
+
+ // Sign it directly to get a signature
+ const signedWithSk = txn.signTxn(signer.sk);
+ const decoded = algosdk.decodeObj(signedWithSk);
+ const signature = decoded.sig;
+
+ // Attach the signature to the transaction indirectly, and compare
+ const signedWithSignature = txn.attachSignature(signer.addr, signature);
+ assert.deepEqual(signedWithSk, signedWithSignature);
+
+ // Check that signer was set
+ const decodedWithSigner = algosdk.decodeObj(signedWithSignature);
+ assert.deepEqual(
+ decodedWithSigner.sgnr,
+ algosdk.decodeAddress(signer.addr).publicKey
+ );
+ });
+
+ it('should not attach signature with incorrect length', () => {
+ const sender = algosdk.generateAccount();
+ const signer = algosdk.generateAccount();
+
+ // Create a transaction
+ const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
+ from: sender.addr,
+ to: signer.addr,
+ amount: 1000,
+ suggestedParams: {
+ firstRound: 12466,
+ lastRound: 13466,
+ genesisID: 'devnet-v33.0',
+ genesisHash: 'JgsgCaCTqIaLeVhyL6XlRu3n7Rfk2FxMeK+wRSaQ7dI=',
+ fee: 4,
+ },
+ });
+
+ // Sign it directly to get a signature
+ const signedWithSk = txn.signTxn(signer.sk);
+ const decoded = algosdk.decodeObj(signedWithSk);
+ const signature = decoded.sig.slice(0, -1); // without the last byte
+
+ // Check that the signature is not attached
+ assert.throws(
+ () => txn.attachSignature(signer.addr, signature),
+ Error('Invalid signature length')
+ );
+ });
});
describe('Multisig Sign', () => {
diff --git a/tests/8.LogicSig.ts b/tests/8.LogicSig.ts
index 9947ac331..e7e7a5fba 100644
--- a/tests/8.LogicSig.ts
+++ b/tests/8.LogicSig.ts
@@ -872,9 +872,7 @@ describe('Program validation', () => {
program[0] = oldVersions[i];
assert.throws(
() => logic.checkProgram(program),
- new Error(
- 'program too costly for Teal version < 4. consider using v4.'
- )
+ new Error('program too costly for version < 4. consider using v4.')
);
}
// new versions
@@ -884,7 +882,7 @@ describe('Program validation', () => {
assert.ok(logic.checkProgram(program));
}
});
- it('should support TEAL v2 opcodes', () => {
+ it('should support AVM v2 opcodes', () => {
assert.ok(logic.langspecEvalMaxVersion >= 2);
assert.ok(logic.langspecLogicSigVersion >= 2);
@@ -904,7 +902,7 @@ describe('Program validation', () => {
result = logic.checkProgram(program);
assert.strictEqual(result, true);
});
- it('should support TEAL v3 opcodes', () => {
+ it('should support AVM v3 opcodes', () => {
assert.ok(logic.langspecEvalMaxVersion >= 3);
assert.ok(logic.langspecLogicSigVersion >= 3);
@@ -954,7 +952,7 @@ describe('Program validation', () => {
]); // int 0; int 1; swap; pop
assert.ok(logic.checkProgram(program));
});
- it('should support TEAL v4 opcodes', () => {
+ it('should support AVM v4 opcodes', () => {
assert.ok(logic.langspecEvalMaxVersion >= 4);
// divmodw
@@ -1073,7 +1071,7 @@ describe('Program validation', () => {
]); // int 1; loop: int 2; *; dup; int 10; <; bnz loop; int 16; ==
assert.ok(logic.checkProgram(program));
});
- it('should support TEAL v5 opcodes', () => {
+ it('should support AVM v5 opcodes', () => {
assert.ok(logic.langspecEvalMaxVersion >= 5);
// itxn ops
@@ -1100,7 +1098,7 @@ describe('Program validation', () => {
// byte "a"; byte "b"; byte "c"; cover 2; uncover 2; concat; concat; log; int 1
assert.ok(logic.checkProgram(program));
});
- it('should support TEAL v6 opcodes', () => {
+ it('should support AVM v6 opcodes', () => {
assert.ok(logic.langspecEvalMaxVersion >= 6);
// bsqrt op
diff --git a/tests/cucumber/steps/steps.js b/tests/cucumber/steps/steps.js
index 5f0607ee5..bfd192f5c 100644
--- a/tests/cucumber/steps/steps.js
+++ b/tests/cucumber/steps/steps.js
@@ -125,6 +125,20 @@ module.exports = function getSteps(options) {
steps.then[name] = fn;
}
+ // Dev Mode State
+ const DEV_MODE_INITIAL_MICROALGOS = 10_000_000;
+
+ /*
+ * waitForAlgodInDevMode is a Dev mode helper method that waits for a transaction to resolve.
+ * Since Dev mode produces blocks on a per transaction basis, it's possible
+ * algod generates a block _before_ the corresponding SDK call to wait for a block.
+ * Without _any_ wait, it's possible the SDK looks for the transaction before algod completes processing.
+ * So, the method performs a local sleep to simulate waiting for a block.
+ */
+ function waitForAlgodInDevMode() {
+ return new Promise((resolve) => setTimeout(resolve, 500));
+ }
+
const { algod_token: algodToken, kmd_token: kmdToken } = options;
Given('an algod client', async function () {
@@ -185,6 +199,26 @@ module.exports = function getSteps(options) {
});
When('I get status after this block', async function () {
+ // Send a transaction to advance blocks in dev mode.
+ const sp = await this.acl.getTransactionParams();
+ if (sp.firstRound === 0) sp.firstRound = 1;
+ const fundingTxnArgs = {
+ from: this.accounts[0],
+ to: this.accounts[0],
+ amount: 0,
+ fee: sp.fee,
+ firstRound: sp.lastRound + 1,
+ lastRound: sp.lastRound + 1000,
+ genesisHash: sp.genesishashb64,
+ genesisID: sp.genesisID,
+ };
+ const stxKmd = await this.kcl.signTransaction(
+ this.handle,
+ this.wallet_pswd,
+ fundingTxnArgs
+ );
+ await this.acl.sendRawTransaction(stxKmd);
+
this.statusAfter = await this.acl.statusAfterBlock(this.status.lastRound);
return this.statusAfter;
});
@@ -359,6 +393,35 @@ module.exports = function getSteps(options) {
return this.pk;
});
+ When(
+ 'I generate a key using kmd for rekeying and fund it',
+ async function () {
+ this.rekey = await this.kcl.generateKey(this.handle);
+ this.rekey = this.rekey.address;
+ // Fund the rekey address with some Algos
+ const sp = await this.acl.getTransactionParams();
+ if (sp.firstRound === 0) sp.firstRound = 1;
+ const fundingTxnArgs = {
+ from: this.accounts[0],
+ to: this.rekey,
+ amount: DEV_MODE_INITIAL_MICROALGOS,
+ fee: sp.fee,
+ firstRound: sp.lastRound + 1,
+ lastRound: sp.lastRound + 1000,
+ genesisHash: sp.genesishashb64,
+ genesisID: sp.genesisID,
+ };
+
+ const stxKmd = await this.kcl.signTransaction(
+ this.handle,
+ this.wallet_pswd,
+ fundingTxnArgs
+ );
+ await this.acl.sendRawTransaction(stxKmd);
+ return this.rekey;
+ }
+ );
+
Then('the key should be in the wallet', async function () {
let keys = await this.kcl.listKeys(this.handle);
keys = keys.addresses;
@@ -431,6 +494,27 @@ module.exports = function getSteps(options) {
}
);
+ Given(
+ 'default transaction with parameters {int} {string} and rekeying key',
+ async function (amt, note) {
+ this.pk = this.rekey;
+ const result = await this.acl.getTransactionParams();
+ this.lastRound = result.lastRound;
+ this.txn = {
+ from: this.rekey,
+ to: this.accounts[1],
+ fee: result.fee,
+ firstRound: result.lastRound + 1,
+ lastRound: result.lastRound + 1000,
+ genesisHash: result.genesishashb64,
+ genesisID: result.genesisID,
+ note: makeUint8Array(Buffer.from(note, 'base64')),
+ amount: parseInt(amt),
+ };
+ return this.txn;
+ }
+ );
+
Given(
'default multisig transaction with parameters {int} {string}',
async function (amt, note) {
@@ -801,13 +885,13 @@ module.exports = function getSteps(options) {
assert.deepStrictEqual(true, 'type' in info);
// let localParams = await this.acl.getTransactionParams();
// this.lastRound = localParams.lastRound;
- await this.acl.statusAfterBlock(this.lastRound + 2);
+ await waitForAlgodInDevMode();
info = await this.acl.transactionById(this.txid);
assert.deepStrictEqual(true, 'type' in info);
});
Then('I can get the transaction by ID', async function () {
- await this.acl.statusAfterBlock(this.lastRound + 2);
+ await waitForAlgodInDevMode();
const info = await this.acl.transactionById(this.txid);
assert.deepStrictEqual(true, 'type' in info);
});
@@ -3900,7 +3984,7 @@ module.exports = function getSteps(options) {
const info = await algosdk.waitForConfirmation(
this.v2Client,
fundingResponse.txId,
- 2
+ 1
);
assert.ok(info['confirmed-round'] > 0);
}
@@ -4256,7 +4340,7 @@ module.exports = function getSteps(options) {
const info = await algosdk.waitForConfirmation(
this.v2Client,
fundingResponse.txId,
- 2
+ 1
);
assert.ok(info['confirmed-round'] > 0);
}
@@ -4377,7 +4461,7 @@ module.exports = function getSteps(options) {
const info = await algosdk.waitForConfirmation(
this.v2Client,
this.appTxid.txId,
- 2
+ 1
);
assert.ok(info['confirmed-round'] > 0);
});