Skip to content

Custom wallet #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Aug 25, 2023
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
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"description": "React hooks for using Algorand compatible wallets in dApps.",
"scripts": {
"dev": "yarn storybook",
"build": "rm -rf dist && rollup -c",
"build": "rimraf dist && rollup -c",
"test": "jest",
"lint": "eslint '**/*.{js,ts,tsx}'",
"format": "prettier --check '**/*.{js,ts,tsx}'",
Expand Down Expand Up @@ -60,6 +60,7 @@
"algosdk": "^2.1.0",
"babel-jest": "^29.1.2",
"babel-loader": "^9.0.0",
"buffer": "^6.0.3",
"commitizen": "4.3.0",
"css-loader": "^6.5.1",
"cz-conventional-changelog": "3.3.0",
Expand All @@ -80,6 +81,7 @@
"react-dom": "^18.2.0",
"release-it": "^16.1.0",
"require-from-string": "^2.0.2",
"rimraf": "^5.0.1",
"rollup": "^3.3.0",
"rollup-plugin-analyzer": "^4.0.0",
"rollup-plugin-dts": "^5.0.0",
Expand Down
104 changes: 104 additions & 0 deletions src/clients/custom/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import Algod, { getAlgodClient } from '../../algod'
import BaseClient from '../base'
import { DEFAULT_NETWORK, PROVIDER_ID } from '../../constants'
import { debugLog } from '../../utils/debugLog'
import { ICON } from './constants'
import type _algosdk from 'algosdk'
import type { Network } from '../../types/node'
import type { InitParams } from '../../types/providers'
import type { Metadata, Wallet } from '../../types/wallet'
import type { CustomProvider, CustomWalletClientConstructor } from './types'

class CustomWalletClient extends BaseClient {
network: Network
providerProxy: CustomProvider

static metadata: Metadata = {
id: PROVIDER_ID.CUSTOM,
icon: ICON,
isWalletConnect: false,
name: 'Custom'
}

constructor({
providerProxy,
metadata,
algosdk,
algodClient,
network
}: CustomWalletClientConstructor) {
super(metadata, algosdk, algodClient)

this.providerProxy = providerProxy
this.network = network
}

static async init({
clientOptions,
algodOptions,
algosdkStatic,
network = DEFAULT_NETWORK
}: InitParams<PROVIDER_ID.CUSTOM>): Promise<BaseClient | null> {
try {
debugLog(`${PROVIDER_ID.CUSTOM.toUpperCase()} initializing...`)

if (!clientOptions) {
throw new Error(`Attempt to create custom wallet with no provider specified.`)
}

const algosdk = algosdkStatic || (await Algod.init(algodOptions)).algosdk
const algodClient = getAlgodClient(algosdk, algodOptions)

try {
return new CustomWalletClient({
providerProxy: clientOptions.getProvider({
algod: algodClient,
algosdkStatic: algosdk,
network
}),
metadata: {
...CustomWalletClient.metadata,
name: clientOptions.name,
icon: clientOptions.icon ?? CustomWalletClient.metadata.icon
},
algodClient,
algosdk,
network
})
} finally {
debugLog(`${PROVIDER_ID.CUSTOM.toUpperCase()} initialized`, '✅')
}
} catch (e) {
console.error('Error initializing...', e)
return null
}
}

async connect(): Promise<Wallet> {
return await this.providerProxy.connect(this.metadata)
}

async disconnect() {
await this.providerProxy.disconnect()
}

async reconnect(): Promise<Wallet | null> {
return await this.providerProxy.reconnect(this.metadata)
}

async signTransactions(
connectedAccounts: string[],
txnGroups: Uint8Array[] | Uint8Array[][],
indexesToSign?: number[],
returnGroup = true
) {
return await this.providerProxy.signTransactions(
connectedAccounts,
txnGroups,
indexesToSign,
returnGroup
)
}
}

export default CustomWalletClient
2 changes: 2 additions & 0 deletions src/clients/custom/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const ICON =
"data:image/svg+xml,%3Csvg fill='%23000000' width='800px' height='800px' viewBox='0 0 24 24' id='wallet' data-name='Flat Line' xmlns='http://www.w3.org/2000/svg' class='icon flat-line'%3E%3Cpath id='secondary' d='M16,12h5V8H5A2,2,0,0,1,3,6V19a1,1,0,0,0,1,1H20a1,1,0,0,0,1-1V16H16a1,1,0,0,1-1-1V13A1,1,0,0,1,16,12Z' style='fill: rgb(44, 169, 188); stroke-width: 2;'%3E%3C/path%3E%3Cpath id='primary' d='M19,4H5A2,2,0,0,0,3,6H3A2,2,0,0,0,5,8H21' style='fill: none; stroke: rgb(0, 0, 0); stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;'%3E%3C/path%3E%3Cpath id='primary-2' data-name='primary' d='M21,8V19a1,1,0,0,1-1,1H4a1,1,0,0,1-1-1V6A2,2,0,0,0,5,8Zm0,4H16a1,1,0,0,0-1,1v2a1,1,0,0,0,1,1h5Z' style='fill: none; stroke: rgb(0, 0, 0); stroke-linecap: round; stroke-linejoin: round; stroke-width: 2;'%3E%3C/path%3E%3C/svg%3E"
3 changes: 3 additions & 0 deletions src/clients/custom/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import custom from './client'

export default custom
33 changes: 33 additions & 0 deletions src/clients/custom/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import type algosdk from 'algosdk'
import type { Network } from '../../types/node'
import type { Metadata, Wallet } from '../../types/wallet'

export type CustomOptions = {
name: string
icon?: string
getProvider: (params: {
network?: Network
algod?: algosdk.Algodv2
algosdkStatic?: typeof algosdk
}) => CustomProvider
}

export type CustomProvider = {
connect(metadata: Metadata): Promise<Wallet>
disconnect(): Promise<void>
reconnect(metadata: Metadata): Promise<Wallet | null>
signTransactions(
connectedAccounts: string[],
txnGroups: Uint8Array[] | Uint8Array[][],
indexesToSign?: number[],
returnGroup?: boolean
): Promise<Uint8Array[]>
}

export type CustomWalletClientConstructor = {
providerProxy: CustomProvider
metadata: Metadata
algosdk: typeof algosdk
algodClient: algosdk.Algodv2
network: Network
}
18 changes: 16 additions & 2 deletions src/clients/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,21 @@ import algosigner from './algosigner'
import walletconnect from './walletconnect2'
import kmd from './kmd'
import mnemonic from './mnemonic'
import { CustomProvider } from './custom/types'
import custom from './custom'

export { pera, myalgo, defly, exodus, algosigner, walletconnect, kmd, mnemonic }
export {
pera,
myalgo,
defly,
exodus,
algosigner,
walletconnect,
kmd,
mnemonic,
custom,
CustomProvider
}

export default {
[pera.metadata.id]: pera,
Expand All @@ -19,5 +32,6 @@ export default {
[algosigner.metadata.id]: algosigner,
[walletconnect.metadata.id]: walletconnect,
[kmd.metadata.id]: kmd,
[mnemonic.metadata.id]: mnemonic
[mnemonic.metadata.id]: mnemonic,
[custom.metadata.id]: custom
}
151 changes: 149 additions & 2 deletions src/components/Example/Example.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,170 @@
import React from 'react'
import { DeflyWalletConnect } from '@blockshake/defly-connect'
import { DaffiWalletConnect } from '@daffiwallet/connect'
import { WalletProvider, PROVIDER_ID, useInitializeProviders } from '../../index'
import { WalletProvider, PROVIDER_ID, useInitializeProviders, Metadata, Network } from '../../index'
import Account from './Account'
import Connect from './Connect'
import Transact from './Transact'
import { CustomProvider } from '../../clients/custom/types'
import algosdk from 'algosdk'
import type _algosdk from 'algosdk'
import { Buffer } from 'buffer'
import { ICON as KMDICON } from '../../clients/kmd/constants'

const getDynamicPeraWalletConnect = async () => {
const PeraWalletConnect = (await import('@perawallet/connect')).PeraWalletConnect
return PeraWalletConnect
}

class TestManualProvider implements CustomProvider {
algosdk: typeof _algosdk

constructor(algosdkStatic: typeof _algosdk) {
this.algosdk = algosdkStatic
}

async connect(metadata: Metadata) {
let address = prompt('Enter address of your account')
if (address && !this.algosdk.isValidAddress(address)) {
alert('Invalid address; please try again')
address = null
}
const authAddress = address
? prompt("Enter address of the signing account; leave blank if account hasn't been rekeyed")
: undefined

return {
...metadata,
accounts: address
? [
{
address,
name: address,
providerId: PROVIDER_ID.CUSTOM,
authAddr: authAddress === null || authAddress === address ? undefined : authAddress
}
]
: []
}
}

async disconnect() {
//
}

async reconnect(_metadata: Metadata) {
return null
}

async signTransactions(
connectedAccounts: string[],
txnGroups: Uint8Array[] | Uint8Array[][],
indexesToSign?: number[] | undefined,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_returnGroup?: boolean | undefined
): Promise<Uint8Array[]> {
// If txnGroups is a nested array, flatten it
const transactions: Uint8Array[] = Array.isArray(txnGroups[0])
? (txnGroups as Uint8Array[][]).flatMap((txn) => txn)
: (txnGroups as Uint8Array[])

// Decode the transactions to access their properties.
const decodedTxns = transactions.map((txn) => {
return this.algosdk.decodeObj(txn)
}) as Array<_algosdk.EncodedTransaction | _algosdk.EncodedSignedTransaction>

const signedTxns: Array<Uint8Array> = []

let idx = -1
for (const dtxn of decodedTxns) {
idx++
const isSigned = 'txn' in dtxn

// push the incoming txn into signed, we'll overwrite it later
signedTxns.push(transactions[idx])

// Its already signed, skip it
if (isSigned) {
continue
// Not specified in indexes to sign, skip it
} else if (indexesToSign && indexesToSign.length && !indexesToSign.includes(Number(idx))) {
continue
}
// Not to be signed by our signer, skip it
else if (!connectedAccounts.includes(this.algosdk.encodeAddress(dtxn.snd))) {
continue
}

const unsignedTxn = this.algosdk.decodeUnsignedTransaction(transactions[idx])

const forSigning = Buffer.from(
this.algosdk.encodeObj({
txn: unsignedTxn.get_obj_for_encoding()
})
).toString('base64')
alert(
`Here is the unsigned transaction bytes that needs signing in base64, press OK to copy to clipboard for signing: ${forSigning}`
)

console.log(forSigning)
// Make async to avoid permission issue
await new Promise<void>((resolve) =>
setTimeout(() => {
navigator.clipboard
.writeText(forSigning)
.catch(() => alert('Copy failed; try copying from developer console'))
resolve()
}, 1)
)

alert(`### Signing instructions ###

1. Check the value landed in your clipboard and if not check the web browser isn't waiting for you to grant permission to clipboard and either way try again
2. Load the value in the clipboard into a file e.g. unsigned.txn:
\`echo {paste value} | base64 -d > unsigned.txn\`
3. Inspect it:
\`goal clerk inspect unsigned.txn\`
4. Sign it e.g.
\`goal clerk sign -i unsigned.txn -o signed.txn\`
5. Output the signed transaction e.g.
\`cat signed.txn | base64\`
6. Copy the signed transaction output to clipboard
7. Press OK here and then paste into the next prompt.`)

const signed = prompt('Provide the base 64 encoded signed transaction')
if (!signed) {
throw new Error('Provided invalid signed transaction')
}

const encoded = Buffer.from(signed, 'base64')
signedTxns[idx] = encoded
}

return signedTxns
}
}

export default function ConnectWallet() {
const walletProviders = useInitializeProviders({
providers: [
{ id: PROVIDER_ID.DEFLY, clientStatic: DeflyWalletConnect },
{ id: PROVIDER_ID.PERA, getDynamicClient: getDynamicPeraWalletConnect },
{ id: PROVIDER_ID.DAFFI, clientStatic: DaffiWalletConnect },
{ id: PROVIDER_ID.EXODUS }
{ id: PROVIDER_ID.EXODUS },
{
id: PROVIDER_ID.CUSTOM,
clientOptions: {
name: 'Manual with KMD icon',
icon: KMDICON,
getProvider: (params: {
network?: Network
algod?: algosdk.Algodv2
algosdkStatic?: typeof algosdk
}) => {
return new TestManualProvider(params.algosdkStatic ?? algosdk)
}
}
}
]
})

Expand Down
1 change: 1 addition & 0 deletions src/constants/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Network } from '../types/node'

export enum PROVIDER_ID {
KMD = 'kmd',
CUSTOM = 'custom',
PERA = 'pera',
DAFFI = 'daffi',
MYALGO = 'myalgo',
Expand Down
Loading