Lute Connect is a Javascript library to securely sign transactions with Lute, an Algorand web wallet and Chrome extension.
The package can be installed via your favorite package manager:
npm i @galaxypay/lute-connectyarn add @galaxypay/lute-connectpnpm add @galaxypay/lute-connectimport LuteConnect from "@galaxypay/lute-connect";
const lute = new LuteConnect();Web mode (no extension, or forceWeb) opens a popup to lute.app. If your site sends Cross-Origin-Opener-Policy, use same-origin-allow-popups; same-origin blocks the popup from replying.
// Warning: Browser will block pop-up if user doesn't trigger lute.connect() with a button click
async function connect() {
try {
const genesis = await algodClient.genesis().do();
const genesisID = `${genesis.network}-${genesis.id}`;
const addresses = await lute.connect(genesisID);
// TODO: handle user address selection and storage
} catch (err) {
console.error(`[LuteWallet] Error connecting: ${err.message}`);
throw err;
}
}To also learn how each account signs, use connectAccounts. It returns the
addresses plus each account's empty signature: base64 of the canonical msgpack
encoding of a SignedTransaction without txn, with placeholder signature
bytes. Use them to simulate fees with correctly shaped signatures, for example
for Falcon (post-quantum) and multisig accounts. An address missing from
emptySignatures is of unknown type.
const { addrs, emptySignatures } = await lute.connectAccounts(genesisID);// Warning: Browser will block pop-up if user doesn't trigger lute.signTxns() with a button click
async function signTransactions(txns) {
try {
const signedTxns = await lute.signTxns(txns);
// TODO: handle signedTxns (e.g. submit to algodClient)
} catch (err) {
console.error(
"[LuteWallet] Error signing transactions: " +
(err instanceof SignTxnsError
? `${err.message} (code: ${err.code})`
: err.message),
);
throw err;
}
}// Warning: Browser will block pop-up if user doesn't trigger lute.signData() with a button click
import { Address } from "algosdk";
import { canonify } from "canonify";
async function authenticate() {
try {
const domain = location.host;
const acctInfo = await algodClient.accountInformation(activeAddress).do();
const siwaRequest: Siwa = {
domain,
chain_id: activeNetworkConfig.caipChainId || "algorand:localnet",
account_address: activeAddress,
type: "ed25519",
statement:
"Put your own statement here, for example: I accept the ExampleOrg Terms of Service.",
uri: location.origin,
version: "1",
nonce: Buffer.from(randomBytes(12)).toString("base64"),
"issued-at": new Date().toISOString(),
};
const dataString = canonify(siwaRequest);
if (!dataString) throw Error("Invalid JSON");
const data = btoa(dataString);
const enc = new TextEncoder();
const authenticatorData = await sha256(enc.encode(domain));
const signer =
acctInfo.authAddr?.publicKey ??
Address.fromString(activeAddress).publicKey;
const sdtSignData: StdSignData = {
data,
signer,
domain,
authenticatorData,
};
const metadata: SignMetadata = {
scope: ScopeType.AUTH,
encoding: "base64",
};
const signerResponse = await lute.signData(sdtSignData, metadata);
// TODO: verify signerResponse
} catch (err) {
console.error(
"[LuteWallet] Error signing data: " +
(err instanceof SignDataError
? `${err.message} (code: ${err.code})`
: err.message),
);
throw err;
}
}A Falcon address is a hash of its public key, so the dApp must send the key itself. Take it from the account's empty signature in the connect result (pqsig.pk). Set type: "falcon-1024-det" on the SIWA request and scheme: "falcon-1024-det" on the StdSignData:
import algosdk from "algosdk";
import { verifyCompressed } from "falcon-1024";
const { pqsig } =
algosdk.decodeSignedTransaction(/* emptySignatures[addr] with a placeholder txn */);
const siwaRequest: Siwa = { ...siwa, type: "falcon-1024-det" };
const sdtSignData: StdSignData = {
data,
signer: pqsig.pk,
domain,
authenticatorData,
scheme: "falcon-1024-det",
};
const resp = await lute.signData(sdtSignData, metadata);
// Verify: the key must hash to the address (or its auth address), and the
// signature must cover sha256(data) || sha256(authenticatorData).
const { address } = algosdk.addressFromPQKey(
algosdk.FALCON_1024_SCHEME,
resp.signer,
);
const toSign = new Uint8Array([
...(await sha256(enc.encode(dataString))),
...(await sha256(authenticatorData)),
]);
const ok =
address.toString() === activeAddress &&
verifyCompressed(resp.signer, resp.signature, toSign);// Warning: Browser will block pop-up if user doesn't trigger lute.addNetwork() with a button click
async function addCustomNetwork() {
try {
const customNetwork: Network = {
name: "ExampleNet",
algod: {
url: "https://examplenet-api.4160.nodely.dev",
port: "",
token: "",
},
genesisID: "examplenet-v1.0",
};
await lute.addNetwork(customNetwork);
} catch (err) {
console.error(
"[LuteWallet] Error adding network: " +
(err instanceof AddNetworkError
? `${err.message} (code: ${err.code})`
: err.message),
);
throw err;
}
}