Skip to content

Commit 354dd4b

Browse files
committed
feat: LogicSignature
1 parent c1d36a8 commit 354dd4b

File tree

8 files changed

+187
-26
lines changed

8 files changed

+187
-26
lines changed

packages/transact/src/index.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,4 @@ export * from './transactions/key-registration'
3434
export * from './transactions/payment'
3535
export * from './signer'
3636

37-
export {
38-
addressFromMultisigSignature,
39-
applyMultisigSubsignature,
40-
mergeMultisignatures,
41-
newMultisigSignature,
42-
participantsFromMultisigSignature,
43-
} from './multisig'
37+
export * from './multisig'

packages/transact/src/logicsig.ts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import { Address, concatArrays, hash, isValidAddress } from '@algorandfoundation/algokit-common'
2+
import { MultisigAccount } from './multisig'
3+
import { MultisigSignature } from './transactions/signed-transaction'
4+
5+
// base64regex is the regex to test for base64 strings
6+
const base64regex = /^([0-9a-zA-Z+/]{4})*(([0-9a-zA-Z+/]{2}==)|([0-9a-zA-Z+/]{3}=))?$/
7+
8+
/** sanityCheckProgram performs heuristic program validation:
9+
* check if passed in bytes are Algorand address or is B64 encoded, rather than Teal bytes
10+
*
11+
* @param program - Program bytes to check
12+
*/
13+
export function sanityCheckProgram(program: Uint8Array) {
14+
if (!program || program.length === 0) throw new Error('empty program')
15+
16+
const lineBreakOrd = '\n'.charCodeAt(0)
17+
const blankSpaceOrd = ' '.charCodeAt(0)
18+
const tildeOrd = '~'.charCodeAt(0)
19+
20+
const isPrintable = (x: number) => blankSpaceOrd <= x && x <= tildeOrd
21+
const isAsciiPrintable = program.every((x: number) => x === lineBreakOrd || isPrintable(x))
22+
23+
if (isAsciiPrintable) {
24+
const programStr = new TextDecoder().decode(program)
25+
26+
if (isValidAddress(programStr)) throw new Error('requesting program bytes, get Algorand address')
27+
28+
if (base64regex.test(programStr)) throw new Error('program should not be b64 encoded')
29+
30+
throw new Error('program bytes are all ASCII printable characters, not looking like Teal byte code')
31+
}
32+
}
33+
34+
const PROGRAM_TAG = new TextEncoder().encode('Program')
35+
const MSIG_PROGRAM_TAG = new TextEncoder().encode('MsigProgram')
36+
const SIGN_PROGRAM_DATA_PREFIX = new TextEncoder().encode('ProgData')
37+
38+
/** Function for signing logic signatures for delegation */
39+
export type DelegatedLsigSigner = (lsig: LogicSig, msig?: MultisigAccount) => Promise<Uint8Array>
40+
41+
/** Function for signing program data for a logic signature */
42+
export type ProgramDataSigner = (data: Uint8Array, lsig: LogicSig) => Promise<Uint8Array>
43+
44+
export class LogicSig {
45+
logic: Uint8Array
46+
args: Uint8Array[]
47+
sig?: Uint8Array
48+
msig?: MultisigSignature
49+
lmsig?: MultisigSignature
50+
51+
constructor(program: Uint8Array, programArgs?: Array<Uint8Array> | null) {
52+
if (programArgs && (!Array.isArray(programArgs) || !programArgs.every((arg) => arg.constructor === Uint8Array))) {
53+
throw new TypeError('Invalid arguments')
54+
}
55+
56+
let args: Uint8Array[] = []
57+
if (programArgs != null) args = programArgs.map((arg) => new Uint8Array(arg))
58+
59+
sanityCheckProgram(program)
60+
61+
this.logic = program
62+
this.args = args
63+
}
64+
65+
/**
66+
* Compute hash of the logic sig program (that is the same as escrow account address) as string address
67+
* @returns String representation of the address
68+
*/
69+
address(): Address {
70+
const toBeSigned = concatArrays(PROGRAM_TAG, this.logic)
71+
const h = hash(toBeSigned)
72+
return new Address(h)
73+
}
74+
75+
async delegate(signer: DelegatedLsigSigner) {
76+
this.sig = await signer(this)
77+
}
78+
79+
async deletegateMultisig(msig: MultisigAccount) {
80+
if (this.lmsig == undefined) {
81+
this.lmsig = {
82+
subsignatures: [],
83+
version: msig.params.version,
84+
threshold: msig.params.threshold,
85+
}
86+
}
87+
for (const addrWithSigner of msig.subSigners) {
88+
const { lsigSigner, addr } = addrWithSigner
89+
const signature = await lsigSigner(this, msig)
90+
91+
this.lmsig.subsignatures.push({ address: addr, signature })
92+
}
93+
}
94+
95+
bytesToSignForDelegation(msig?: MultisigAccount): Uint8Array {
96+
if (msig) {
97+
return concatArrays(MSIG_PROGRAM_TAG, msig.addr.publicKey, this.logic)
98+
} else {
99+
return concatArrays(PROGRAM_TAG, this.logic)
100+
}
101+
}
102+
103+
signProgramData(data: Uint8Array, signer: ProgramDataSigner): Promise<Uint8Array> {
104+
return signer(data, this)
105+
}
106+
107+
programDataToSign(data: Uint8Array): Uint8Array {
108+
return concatArrays(SIGN_PROGRAM_DATA_PREFIX, this.address().publicKey, data)
109+
}
110+
}

packages/transact/src/multisig.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ describe('multisig', () => {
106106
'ALGOC4J2BCZ33TCKSSAMV5GAXQBMV3HDCHDBSPRBZRNSR7BM2FFDZRFGXA',
107107
].map(Address.fromString)
108108

109-
const multisigLarge = newMultisigSignature(300, 2, participants)
109+
const multisigLarge = newMultisigSignature(254, 2, participants)
110110
const addressLarge = addressFromMultisigSignature(multisigLarge)
111111

112112
// Should be different from the original small values

packages/transact/src/multisig.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
SIGNATURE_BYTE_LENGTH,
1010
} from '@algorandfoundation/algokit-common'
1111
import {
12+
AddressWithDelegatedLsigSigner,
1213
AddressWithTransactionSigner,
1314
decodeSignedTransaction,
1415
encodeSignedTransaction,
@@ -396,9 +397,9 @@ export function addressFromMultisigPreImg({
396397
}: Omit<MultisigMetadata, 'addrs'> & {
397398
pks: Uint8Array[]
398399
}): Address {
399-
if (version !== 1 || version > 255 || version < 0) {
400+
if (version > 255 || version < 0) {
400401
// ^ a tad redundant, but in case in the future version != 1, still check for uint8
401-
throw new Error(INVALID_MSIG_VERSION_ERROR_MSG)
402+
throw new Error(`${INVALID_MSIG_VERSION_ERROR_MSG}: ${version}`)
402403
}
403404
if (threshold === 0 || pks.length === 0 || threshold > pks.length || threshold > 255) {
404405
throw new Error(INVALID_MSIG_THRESHOLD_ERROR_MSG)
@@ -458,13 +459,13 @@ export interface MultisigMetadata {
458459
/**
459460
* A list of Algorand addresses representing possible signers for this multisig. Order is important.
460461
*/
461-
addrs: Array<string | Address>
462+
addrs: Array<Address>
462463
}
463464

464465
/** Account wrapper that supports partial or full multisig signing. */
465466
export class MultisigAccount implements AddressWithTransactionSigner {
466467
_params: MultisigMetadata
467-
_subSigners: AddressWithTransactionSigner[]
468+
_subSigners: (AddressWithTransactionSigner & AddressWithDelegatedLsigSigner)[]
468469
_addr: Address
469470
_signer: TransactionSigner
470471

@@ -473,8 +474,8 @@ export class MultisigAccount implements AddressWithTransactionSigner {
473474
return this._params
474475
}
475476

476-
/** The list of accounts that are present to sign */
477-
get signingAccounts(): Readonly<AddressWithTransactionSigner[]> {
477+
/** The list of accounts that are present to sign transactions or lsigs */
478+
get subSigners() {
478479
return this._subSigners
479480
}
480481

@@ -488,7 +489,7 @@ export class MultisigAccount implements AddressWithTransactionSigner {
488489
return this._signer
489490
}
490491

491-
constructor(multisigParams: MultisigMetadata, subSigners: AddressWithTransactionSigner[]) {
492+
constructor(multisigParams: MultisigMetadata, subSigners: (AddressWithTransactionSigner & AddressWithDelegatedLsigSigner)[]) {
492493
this._params = multisigParams
493494
this._subSigners = subSigners
494495
this._addr = multisigAddress(multisigParams)
@@ -499,7 +500,7 @@ export class MultisigAccount implements AddressWithTransactionSigner {
499500
for (const txn of txnsToSign) {
500501
let signedMsigTxn = createMultisigTransaction(txn, this._params)
501502

502-
for (const subSigner of this._subSigners) {
503+
for (const subSigner of this.subSigners) {
503504
const stxn = (await subSigner.signer([txn], [0]))[0]
504505
const sig = decodeSignedTransaction(stxn).signature
505506

packages/transact/src/signer.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,65 @@
1-
import { Addressable, ReadableAddress } from '@algorandfoundation/algokit-common'
2-
import { Transaction } from './transactions/transaction'
1+
import { Address, Addressable, ReadableAddress } from '@algorandfoundation/algokit-common'
2+
import { encodeTransaction, Transaction } from './transactions/transaction'
3+
import { DelegatedLsigSigner, ProgramDataSigner } from './logicsig'
4+
import { encodeSignedTransaction, SignedTransaction } from './transactions/signed-transaction'
35

6+
/** Function for signing a group of transactions */
47
export type TransactionSigner = (txnGroup: Transaction[], indexesToSign: number[]) => Promise<Uint8Array[]>
58

9+
/** A transaction signer attached to an address */
610
export interface AddressWithTransactionSigner extends Addressable {
711
signer: TransactionSigner
812
}
13+
14+
/** An address that can be used to send transactions that may or may not have a signer */
915
export type SendingAddress = ReadableAddress | AddressWithTransactionSigner
16+
17+
/** A delegated logic signature signer attached to an address */
18+
export interface AddressWithDelegatedLsigSigner extends Addressable {
19+
lsigSigner: DelegatedLsigSigner
20+
}
21+
22+
export type AddressWithSigners = Addressable &
23+
AddressWithTransactionSigner &
24+
AddressWithDelegatedLsigSigner & { programDataSigner: ProgramDataSigner }
25+
26+
/** Generate type-safe domain-separated signer callbacks given an ed25519 pubkey and a signing callback */
27+
export function generateSigners(
28+
ed25519Pubkey: Uint8Array,
29+
rawEd25519Signer: (bytesToSign: Uint8Array) => Promise<Uint8Array>,
30+
): AddressWithSigners {
31+
const addr = new Address(ed25519Pubkey)
32+
33+
const signer: TransactionSigner = async (txnGroup: Transaction[], indexesToSign: number[]) => {
34+
const stxns: SignedTransaction[] = []
35+
for (const index of indexesToSign) {
36+
const txn = txnGroup[index]
37+
const bytesToSign = encodeTransaction(txn)
38+
const signature = await rawEd25519Signer(bytesToSign)
39+
const stxn: SignedTransaction = {
40+
txn,
41+
signature,
42+
}
43+
44+
if (!txn.sender.equals(addr)) {
45+
stxn.authAddress = addr
46+
}
47+
48+
stxns.push(stxn)
49+
}
50+
51+
return stxns.map(encodeSignedTransaction)
52+
}
53+
54+
const lsigSigner: DelegatedLsigSigner = async (lsig, msig) => {
55+
const bytesToSign = lsig.bytesToSignForDelegation(msig)
56+
return await rawEd25519Signer(bytesToSign)
57+
}
58+
59+
const programDataSigner: ProgramDataSigner = async (data, lsig) => {
60+
const toBeSigned = lsig.programDataToSign(data)
61+
return await rawEd25519Signer(toBeSigned)
62+
}
63+
64+
return { addr, signer, lsigSigner, programDataSigner }
65+
}

src/testing/account.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import * as algosdk from '@algorandfoundation/sdk'
44
import { Address, Kmd } from '@algorandfoundation/sdk'
55
import { AlgorandClient, Config } from '../'
66
import { GetTestAccountParams } from '../types/testing'
7-
import { AddressWithTransactionSigner } from '@algorandfoundation/algokit-transact'
7+
import { AddressWithSigners, AddressWithTransactionSigner } from '@algorandfoundation/algokit-transact'
88

99
/**
1010
* @deprecated Use `getTestAccount(params, algorandClient)` instead. The `algorandClient` object can be created using `AlgorandClient.fromClients({ algod, kmd })`.
@@ -35,7 +35,7 @@ export async function getTestAccount(
3535
export async function getTestAccount(
3636
params: GetTestAccountParams,
3737
algorand: AlgorandClient,
38-
): Promise<Address & Account & AddressWithTransactionSigner>
38+
): Promise<Address & Account & AddressWithSigners>
3939
export async function getTestAccount(
4040
{ suppressLog, initialFunds, accountGetter }: GetTestAccountParams,
4141
algodOrAlgorandClient: AlgodClient | AlgorandClient,

src/types/account-manager.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ import { CommonTransactionParams, TransactionComposer } from './composer'
1111
import { TestNetDispenserApiClient } from './dispenser-client'
1212
import { KmdAccountManager } from './kmd-account-manager'
1313
import { SendParams, SendSingleTransactionResult } from './transaction'
14-
import { AddressWithTransactionSigner, TransactionSigner } from '@algorandfoundation/algokit-transact'
14+
import { AddressWithSigners, AddressWithTransactionSigner, TransactionSigner } from '@algorandfoundation/algokit-transact'
1515
import { getAddress, ReadableAddress } from '@algorandfoundation/algokit-common'
16-
import { MultisigAccount } from '@algorandfoundation/algokit-transact/multisig'
16+
import { MultisigAccount, MultisigMetadata } from '@algorandfoundation/algokit-transact'
1717

1818
/** Result from performing an ensureFunded call. */
1919
export interface EnsureFundedResult {
@@ -393,7 +393,7 @@ export class AccountManager {
393393
* @param subSigners The signers that are currently present
394394
* @returns A multisig account wrapper
395395
*/
396-
public multisig(multisigParams: algosdk.MultisigMetadata, subSigners: AddressWithTransactionSigner[]) {
396+
public multisig(multisigParams: MultisigMetadata, subSigners: AddressWithSigners[]) {
397397
return this.signerAccount(new MultisigAccount(multisigParams, subSigners))
398398
}
399399

src/types/testing.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AlgodClient } from '@algorandfoundation/algokit-algod-client'
2-
import { AddressWithTransactionSigner, Transaction } from '@algorandfoundation/algokit-transact'
2+
import { AddressWithSigners, AddressWithTransactionSigner, Transaction } from '@algorandfoundation/algokit-transact'
33
import type { Account } from '@algorandfoundation/sdk'
44
import * as algosdk from '@algorandfoundation/sdk'
55
import { Address, Indexer, Kmd, LogicSigAccount } from '@algorandfoundation/sdk'
@@ -26,9 +26,9 @@ export interface AlgorandTestAutomationContext {
2626
/** Transaction logger that will log transaction IDs for all transactions issued by `algod` */
2727
transactionLogger: TransactionLogger
2828
/** Default, funded test account that is ephemerally created */
29-
testAccount: Address & AddressWithTransactionSigner & Account
29+
testAccount: Address & AddressWithSigners & Account
3030
/** Generate and fund an additional ephemerally created account */
31-
generateAccount: (params: GetTestAccountParams) => Promise<Address & Account & AddressWithTransactionSigner>
31+
generateAccount: (params: GetTestAccountParams) => Promise<Address & Account & AddressWithSigners>
3232
/** Wait for the indexer to catch up with all transactions logged by `transactionLogger` */
3333
waitForIndexer: () => Promise<void>
3434
/** Wait for the indexer to catch up with the given transaction ID */

0 commit comments

Comments
 (0)