Skip to content


We are looking for publications that demonstrate building dApps or smart contracts!
See the full list of Gitcoin bounties that are eligible for rewards.

Offline signatures

This section explains how to authorize transactions with private keys that are kept offline. In particular, this guide shows how to create and save transactions to a file that can then be transferred to an offline device for signing. To learn about the structure of transactions and how to authorize them in general visit the Transactions Structure and Authorizing Transactions sections, respectively.

The same methodology described here can also be used to work with LogicSignatures and Multisignatures. All objects in the following examples use msgpack to store the transaction object ensuring interoperability with the SDKs and goal.


Storing keys offline is also referred to as placing them in cold storage. An online device that stores private keys is often referred to as a hot wallet.

Unsigned Transaction File Operations

Algorand SDK's and goal support writing and reading both signed and unsigned transactions to a file. Examples of these scenarios are shown in the following code snippets.

Unsigned transactions require the transaction object to be created before writing to a file.

const txn = algosdk.makePaymentTxnWithSuggestedParamsFromObject({
  from: sender.addr,
  to: receiver.addr,
  amount: 1e6,

const txnBytes = algosdk.encodeUnsignedTransaction(txn);
const txnB64 = Buffer.from(txnBytes).toString('base64');
// ...
const restoredTxn = algosdk.decodeUnsignedTransaction(
  Buffer.from(txnB64, 'base64')
Snippet Source

sp = algod_client.suggested_params()
pay_txn = transaction.PaymentTxn(acct.address, sp, acct.address, 10000)

# Write message packed transaction to disk
with open("pay.txn", "w") as f:

# Read message packed transaction and decode it to a Transaction object
with open("pay.txn", "r") as f:
    recovered_txn = encoding.msgpack_decode(

Snippet Source

Response<TransactionParametersResponse> rsp = algodClient.TransactionParams().execute();
TransactionParametersResponse sp = rsp.body();
Transaction ptxn = Transaction.PaymentTransactionBuilder().suggestedParams(sp)

byte[] encodedTxn = Encoder.encodeToMsgPack(ptxn);

Transaction decodedTxn = Encoder.decodeFromMsgPack(encodedTxn, Transaction.class);
assert decodedTxn.equals(ptxn);
Snippet Source

// Error handling omitted for brevity
sp, _ := algodClient.SuggestedParams().Do(context.Background())
ptxn, _ := transaction.MakePaymentTxn(
    acct1.Address.String(), acct1.Address.String(), 10000, nil, "", sp,

// Encode the txn as bytes,
// if sending over the wire (like to a frontend) it should also be b64 encoded
encodedTxn := msgpack.Encode(ptxn)
os.WriteFile("pay.txn", encodedTxn, 0655)

var recoveredPayTxn = types.Transaction{}

msgpack.Decode(encodedTxn, &recoveredPayTxn)
log.Printf("%+v", recoveredPayTxn)
Snippet Source

$ goal clerk send --from=<my-account> --to=GD64YIY3TWGDMCNPP553DZPPR6LDUSFQOIJVFDPPXWEG3FVOJCCDBBHU5A --fee=1000 --amount=1000000 --out="unsigned.txn"

$ goal clerk sign --infile unsigned.txn --outfile signed.txn

$ goal clerk rawsend --filename signed.txn

Signed Transaction File Operations

Signed Transactions are similar, but require an account to sign the transaction before writing it to a file.

const signedTxn = txn.signTxn(sender.privateKey);
const signedB64Txn = Buffer.from(signedTxn).toString('base64');
const restoredSignedTxn = algosdk.decodeSignedTransaction(
  Buffer.from(signedB64Txn, 'base64')
Snippet Source

# Sign transaction
spay_txn = pay_txn.sign(acct.private_key)
# write message packed signed transaction to disk
with open("signed_pay.txn", "w") as f:

# read message packed signed transaction into a SignedTransaction object
with open("signed_pay.txn", "r") as f:
    recovered_signed_txn = encoding.msgpack_decode(

Snippet Source

SignedTransaction signedTxn = acct.signTransaction(ptxn);
byte[] encodedSignedTxn = Encoder.encodeToMsgPack(signedTxn);

SignedTransaction decodedSignedTransaction = Encoder.decodeFromMsgPack(encodedSignedTxn,
assert decodedSignedTransaction.equals(signedTxn);
Snippet Source

// Assuming we already have a pay transaction `ptxn`

// Sign the transaction
_, signedTxn, err := crypto.SignTransaction(acct1.PrivateKey, ptxn)
if err != nil {
    log.Fatalf("failed to sign transaction: %s", err)

// Save the signed transaction to file
os.WriteFile("pay.stxn", signedTxn, 0644)

signedPayTxn := types.SignedTxn{}
err = msgpack.Decode(signedTxn, &signedPayTxn)
if err != nil {
    log.Fatalf("failed to decode signed transaction: %s", err)
Snippet Source

$ goal clerk rawsend --filename signed.txn

Signature Verification

Sometimes a transaction is signed by a third party, and you want to verify that the signature is valid. This can be done by decoding the signed transaction into a SignedTransaction object using one of the SDKs and then running and ed25519 verify on the signature.

const stxn = algosdk.decodeSignedTransaction(rawSignedTxn);
if (stxn.sig === undefined) return false;

// Get the txn object
const txnObj = stxn.txn.get_obj_for_encoding();
if (txnObj === undefined) return false;

// Encode as msgpack
const txnBytes = algosdk.encodeObj(txnObj);
// Create byte array with TX prefix
const msgBytes = new Uint8Array(txnBytes.length + 2);
msgBytes.set(txnBytes, 2);

// Grab the other things we need to verify
const pkBytes = stxn.txn.from.publicKey;
const sigBytes = new Uint8Array(stxn.sig);

// Return the result of the verification
const valid = nacl.sign.detached.verify(msgBytes, sigBytes, pkBytes);
console.log('Valid? ', valid);
Snippet Source

# decode the signed transaction
stxn = encoding.msgpack_decode(raw_stxn)
if stxn.signature is None or len(stxn.signature) == 0:
    return False

public_key = stxn.transaction.sender
if stxn.authorizing_address is not None:
    public_key = stxn.authorizing_address

# Create a VerifyKey from nacl using the 32 byte public key
verify_key = VerifyKey(encoding.decode_address(public_key))

# Generate the message that was signed with TX prefix
prefixed_message = b"TX" + base64.b64decode(

    # Verify the signature
    verify_key.verify(prefixed_message, base64.b64decode(stxn.signature))
    return True
except BadSignatureError:
    return False
Snippet Source

signedTxn := types.SignedTxn{}
msgpack.Decode(stxn, &signedTxn)

from := signedTxn.Txn.Sender[:]

encodedTx := msgpack.Encode(signedTxn.Txn)

msgParts := [][]byte{txidPrefix, encodedTx}
msg := bytes.Join(msgParts, nil)

valid := ed25519.Verify(from, msg, signedTxn.Sig[:])

log.Printf("Valid? %t", valid)
Snippet Source

// decode the signature
SignedTransaction decodedSignedTransaction = Encoder.decodeFromMsgPack(rawSignedTxn,
Transaction txn = decodedSignedTransaction.tx;

// get the bytes that were signed
byte[] signedBytes = txn.bytesToSign();
// get the pubkey that signed them
PublicKey pk = txn.sender.toVerifyKey();

// set up the sig checker sigChecker ="Ed25519");
// verify the signature 
boolean valid = sigChecker.verify(decodedSignedTransaction.sig.getBytes());
System.out.printf("Valid? %b\n", valid);
Snippet Source

Saving Signed and Unsigned Multisig Transactions to a File using goal

Create a multisig account by listing all of the accounts in the multisig and specifying the threshold number of accounts to sign with the -T flag

goal account multisig new <my-account1> <my-account2> <my-account3> etc… -T 2    

Create an unsigned transaction and write to file

goal clerk send --from <my-multisig-account>  --to AZLR2XP4O2WFHLX6TX7AZVY23HLVLG3K5K3FRIKIYDOYN6ISIF54SA4RNY --fee=1000 --amount=1000000 --out="unsigned.txn"

Sign by the required number of accounts to meet the threshold.

goal clerk multisig sign -a F <my-account1> -t=unsigned.txn
goal clerk multisig sign -a F <my-account2> -t=unsigned.txn

Merge signings

goal clerk multisig merge --out signed.txn unsigned.txn


goal clerk rawsend --filename signed.txn