Skip to content

Commit

Permalink
TXN-1277: Migrate to AlgoSigner v1.10.0, support rekeyed signing (#53)
Browse files Browse the repository at this point in the history
  • Loading branch information
drichar authored Mar 17, 2023
1 parent 77b8f03 commit fbff1ee
Show file tree
Hide file tree
Showing 4 changed files with 114 additions and 76 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"url": "https://github.com/txnlab/use-wallet/issues"
},
"homepage": "https://txnlab.github.io/use-wallet",
"version": "1.2.6",
"version": "1.2.7",
"description": "React hooks for using Algorand compatible wallets in dApps.",
"scripts": {
"dev": "yarn storybook",
Expand Down
138 changes: 78 additions & 60 deletions src/clients/algosigner/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,36 +11,22 @@ import { ICON } from './constants'
import type {
WindowExtended,
AlgoSignerTransaction,
SupportedLedgers,
AlgoSigner,
AlgoSignerClientConstructor,
InitParams
} from './types'

const getNetwork = (network: string): SupportedLedgers => {
if (network === 'betanet') {
return 'BetaNet'
}

if (network === 'testnet') {
return 'TestNet'
}

if (network === 'mainnet') {
return 'MainNet'
}

return network
}
import { useWalletStore } from '../../store'

class AlgoSignerClient extends BaseWallet {
#client: AlgoSigner
network: Network
walletStore: typeof useWalletStore

constructor({ metadata, client, algosdk, algodClient, network }: AlgoSignerClientConstructor) {
super(metadata, algosdk, algodClient)
this.#client = client
this.network = network
this.walletStore = useWalletStore
}

static metadata = {
Expand All @@ -52,13 +38,13 @@ class AlgoSignerClient extends BaseWallet {

static async init({ algodOptions, algosdkStatic, network = DEFAULT_NETWORK }: InitParams) {
try {
if (typeof window == 'undefined' || (window as WindowExtended).AlgoSigner === undefined) {
if (typeof window == 'undefined' || (window as WindowExtended).algorand === undefined) {
throw new Error('AlgoSigner is not available.')
}

const algosdk = algosdkStatic || (await Algod.init(algodOptions)).algosdk
const algodClient = getAlgodClient(algosdk, algodOptions)
const algosigner = (window as WindowExtended).AlgoSigner
const algosigner = (window as WindowExtended).algorand

return new AlgoSignerClient({
metadata: AlgoSignerClient.metadata,
Expand All @@ -80,21 +66,28 @@ class AlgoSignerClient extends BaseWallet {
}

async connect() {
await this.#client.connect()

const accounts = await this.#client.accounts({
ledger: getNetwork(this.network)
})
const { accounts } = await this.#client.enable({ genesisID: this.getGenesisID() })

if (accounts.length === 0) {
throw new Error(`No accounts found for ${AlgoSignerClient.metadata.id}`)
}

const mappedAccounts = accounts.map(({ address }, index) => ({
name: `AlgoSigner ${index + 1}`,
address,
providerId: AlgoSignerClient.metadata.id
}))
const mappedAccounts = await Promise.all(
accounts.map(async (address, index) => {
// check to see if this is a rekeyed account
const { 'auth-addr': authAddr } = await this.getAccountInfo(address)

return {
name: `AlgoSigner ${index + 1}`,
address,
providerId: AlgoSignerClient.metadata.id,
...(authAddr && { authAddr })
}
})
)

// sort the accounts in the order they were returned by AlgoSigner
mappedAccounts.sort((a, b) => accounts.indexOf(a.address) - accounts.indexOf(b.address))

return {
...AlgoSignerClient.metadata,
Expand All @@ -104,7 +97,7 @@ class AlgoSignerClient extends BaseWallet {

// eslint-disable-next-line @typescript-eslint/require-await
async reconnect(onDisconnect: () => void) {
if (window === undefined || (window as WindowExtended).AlgoSigner === undefined) {
if (window === undefined || (window as WindowExtended).algorand === undefined) {
onDisconnect()
}

Expand All @@ -127,57 +120,82 @@ class AlgoSignerClient extends BaseWallet {
return this.algosdk.decodeObj(txn)
}) as Array<DecodedTransaction | DecodedSignedTransaction>

const signedIndexes: number[] = []

// Marshal the transactions,
// and add the signers property if they shouldn't be signed.
const txnsToSign = decodedTxns.reduce<AlgoSignerTransaction[]>((acc, txn, i) => {
const isSigned = 'txn' in txn

const txnObj: AlgoSignerTransaction = {
txn: this.#client.encoding.msgpackToBase64(transactions[i])
const sender = this.algosdk.encodeAddress(isSigned ? txn.txn.snd : txn.snd)
const authAddress = this.getAuthAddress(sender) // rekeyed-to account, or undefined

if (indexesToSign && indexesToSign.length && indexesToSign.includes(i)) {
signedIndexes.push(i)
acc.push({
txn: this.#client.encoding.msgpackToBase64(transactions[i]),
...(authAddress && { authAddr: authAddress })
})
} else if (!isSigned && connectedAccounts.includes(sender)) {
signedIndexes.push(i)
acc.push({
txn: this.#client.encoding.msgpackToBase64(transactions[i]),
...(authAddress && { authAddr: authAddress })
})
} else {
acc.push({
txn: this.#client.encoding.msgpackToBase64(
isSigned
? this.algosdk.decodeSignedTransaction(transactions[i]).txn.toByte()
: this.algosdk.decodeUnsignedTransaction(transactions[i]).toByte()
),
signers: []
})
}

if (indexesToSign && indexesToSign.length && !indexesToSign.includes(i)) {
txnObj.txn = this.#client.encoding.msgpackToBase64(
isSigned
? this.algosdk.decodeSignedTransaction(transactions[i]).txn.toByte()
: this.algosdk.decodeUnsignedTransaction(transactions[i]).toByte()
)
txnObj.signers = []
} else if (
!connectedAccounts.includes(
this.algosdk.encodeAddress(isSigned ? txn.txn['snd'] : txn['snd'])
)
) {
txnObj.txn = this.#client.encoding.msgpackToBase64(
isSigned
? this.algosdk.decodeSignedTransaction(transactions[i]).txn.toByte()
: this.algosdk.decodeUnsignedTransaction(transactions[i]).toByte()
)
txnObj.signers = []
}

acc.push(txnObj)

return acc
}, [])

// Sign them with the client.
const result = await this.#client.signTxn(txnsToSign)
const result = await this.#client.signTxns(txnsToSign)

// Join the newly signed transactions with the original group of transactions
// if 'returnGroup' param is specified
const signedTxns = result.reduce<Uint8Array[]>((acc, txn, i) => {
if (txn) {
acc.push(new Uint8Array(Buffer.from(txn.blob, 'base64')))
// if `returnGroup` is true
const signedTxns = transactions.reduce<Uint8Array[]>((acc, txn, i) => {
if (signedIndexes.includes(i)) {
const signedByUser = result[i]
signedByUser && acc.push(new Uint8Array(Buffer.from(signedByUser, 'base64')))
} else if (returnGroup) {
acc.push(transactions[i])
acc.push(txn)
}

return acc
}, [])

return signedTxns
}

getGenesisID() {
if (this.network === 'betanet') {
return 'betanet-v1.0'
}
if (this.network === 'testnet') {
return 'testnet-v1.0'
}
if (this.network === 'mainnet') {
return 'mainnet-v1.0'
}
return this.network
}

getAuthAddress(address: string): string | undefined {
const accounts = this.walletStore.getState().accounts
const account = accounts.find(
(acct) => acct.address === address && acct.providerId === this.metadata.id
)

return account?.authAddr
}
}

export default AlgoSignerClient
46 changes: 32 additions & 14 deletions src/clients/algosigner/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,28 +2,46 @@ import type _algosdk from 'algosdk'
import { PROVIDER_ID } from '../../constants'
import type { AlgodClientOptions, Network, Metadata } from '../../types'

export type WindowExtended = { AlgoSigner: AlgoSigner } & Window & typeof globalThis
export type WindowExtended = { algorand: AlgoSigner } & Window & typeof globalThis

export type GenesisId = 'betanet-v1.0' | 'testnet-v1.0' | 'mainnet-v1.0' | string

export type EnableParams = {
// specific genesis ID requested by the dApp
genesisID?: GenesisId
// specific genesis hash requested by the dApp
genesisHash?: string
// array of specific accounts requested by the dApp
accounts?: string[]
}

export type EnableResponse = {
// specific genesis ID shared by the user
genesisID: GenesisId
// specific genesis hash shared by the user
genesisHash: string
// array of specific accounts shared by the user
accounts: string[]
}

export type AlgoSignerTransaction = {
// Base64-encoded string of a transaction binary
txn: string
// array of addresses to sign with (defaults to the sender),
// setting this to an empty array tells AlgoSigner
// that this transaction is not meant to be signed
signers?: []
multisig?: string // address of a multisig wallet to sign with
signers?: string[]
// Base64-encoded string of a signed transaction binary
stxn?: string
// address of a multisig wallet to sign with
multisig?: string
// used to specify which account is doing the signing when dealing with rekeyed accounts
authAddr?: string
}

export type SupportedLedgers = 'MainNet' | 'TestNet' | 'BetaNet' | string

export type AlgoSigner = {
connect: () => Promise<Record<string, never>>
accounts: (ledger: { ledger: SupportedLedgers }) => Promise<{ address: string }[]>
signTxn: (transactions: AlgoSignerTransaction[]) => Promise<
{
txID: string
blob: string
}[]
>
enable: (params?: EnableParams) => Promise<EnableResponse>
signTxns: (transactions: AlgoSignerTransaction[]) => Promise<string[]>
encoding: {
msgpackToBase64(transaction: Uint8Array): string
byteArrayToString(transaction: Uint8Array): string
Expand All @@ -36,7 +54,7 @@ export type AlgoSignerClientConstructor = {
id: PROVIDER_ID
algosdk: typeof _algosdk
algodClient: _algosdk.Algodv2
network: SupportedLedgers
network: Network
}

export type InitParams = {
Expand Down
4 changes: 3 additions & 1 deletion src/types/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface Account {
providerId: PROVIDER_ID
name: string
address: string
authAddr?: string
}

export type Provider = {
Expand Down Expand Up @@ -34,8 +35,9 @@ export type Asset = {
export type AccountInfo = {
address: string
amount: number
assets: Asset[]
'min-balance': number
'auth-addr'?: string
assets?: Asset[]
}

export type WalletProvider = {
Expand Down

0 comments on commit fbff1ee

Please sign in to comment.