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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ Include a minified browser bundle directly in your HTML like so:

```html
<script
src="https://unpkg.com/algosdk@v1.19.0/dist/browser/algosdk.min.js"
integrity="sha384-5cebCuqDx6A5Y1HeScIKIcSdqsub2M3wwkqTZyu45M8zN/+do8cgxcHDJjkaVTNb"
src="https://unpkg.com/algosdk@v1.19.1/dist/browser/algosdk.min.js"
integrity="sha384-vpY7inPLTrCOYSwaOYlQbFwoSY/t3lFMVjAh/iXN+86fNAQ39DeQjlX87aczChqD"
crossorigin="anonymous"
></script>
```
Expand All @@ -32,8 +32,8 @@ or

```html
<script
src="https://cdn.jsdelivr.net/npm/algosdk@v1.19.0/dist/browser/algosdk.min.js"
integrity="sha384-5cebCuqDx6A5Y1HeScIKIcSdqsub2M3wwkqTZyu45M8zN/+do8cgxcHDJjkaVTNb"
src="https://cdn.jsdelivr.net/npm/algosdk@v1.19.1/dist/browser/algosdk.min.js"
integrity="sha384-vpY7inPLTrCOYSwaOYlQbFwoSY/t3lFMVjAh/iXN+86fNAQ39DeQjlX87aczChqD"
crossorigin="anonymous"
></script>
```
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
4 changes: 1 addition & 3 deletions src/logic/logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
Expand Down
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,9 @@ export {
signMultisigTransaction,
mergeMultisigTransactions,
appendSignMultisigTransaction,
createMultisigTransaction,
appendSignRawMultisigSignature,
verifyMultisig,
multisigAddress,
} from './multisig';
export { SourceMap } from './logic/sourcemap';
Expand Down
154 changes: 127 additions & 27 deletions src/multisig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -40,41 +42,27 @@ interface MultisigMetadataWithPks extends Omit<MultisigMetadata, 'addrs'> {
}

/**
* 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,
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
*/
Expand Down
4 changes: 4 additions & 0 deletions src/nacl/naclWrappers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
16 changes: 16 additions & 0 deletions src/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));
Expand Down
8 changes: 8 additions & 0 deletions tests/4.Utils.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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);
});
});
Loading