diff --git a/package-lock.json b/package-lock.json index dcf2de10a..0c7835158 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "@noble/hashes": "1.6.0", "@scure/base": "1.2.1", "@scure/starknet": "1.1.0", + "@starknet-io/get-starknet-wallet-standard": "^5.0.0-beta.0", "@starknet-io/starknet-types-08": "npm:@starknet-io/types-js@~0.8.4", "@starknet-io/starknet-types-09": "npm:@starknet-io/types-js@beta", "abi-wan-kanabi": "2.2.4", @@ -68,6 +69,12 @@ "node": ">=22" } }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.11.0.tgz", + "integrity": "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg==", + "license": "MIT" + }, "node_modules/@ampproject/remapping": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", @@ -4171,6 +4178,90 @@ "url": "https://paulmillr.com/funding/" } }, + "node_modules/@scure/bip32": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@scure/bip32/-/bip32-1.7.0.tgz", + "integrity": "sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==", + "license": "MIT", + "dependencies": { + "@noble/curves": "~1.9.0", + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/curves": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.9.4.tgz", + "integrity": "sha512-2bKONnuM53lINoDrSmK8qP8W271ms7pygDhZt4SiLOoLwBtoHqeCFi6RG42V8zd3mLHuJFhU/Bmaqo4nX0/kBw==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "1.8.0" + }, + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip32/node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@scure/bip39/-/bip39-1.6.0.tgz", + "integrity": "sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==", + "license": "MIT", + "dependencies": { + "@noble/hashes": "~1.8.0", + "@scure/base": "~1.2.5" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@noble/hashes": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.8.0.tgz", + "integrity": "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==", + "license": "MIT", + "engines": { + "node": "^14.21.3 || >=16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@scure/bip39/node_modules/@scure/base": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-1.2.6.tgz", + "integrity": "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==", + "license": "MIT", + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/@scure/starknet": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@scure/starknet/-/starknet-1.1.0.tgz", @@ -4869,6 +4960,18 @@ "@sinonjs/commons": "^3.0.0" } }, + "node_modules/@starknet-io/get-starknet-wallet-standard": { + "version": "5.0.0-beta.0", + "resolved": "https://registry.npmjs.org/@starknet-io/get-starknet-wallet-standard/-/get-starknet-wallet-standard-5.0.0-beta.0.tgz", + "integrity": "sha512-1QBmWnd76KzKwnVlRXLh58wtd1e0P5fyOhs0bCCjivTpGJ2nUPRvTgmzVJStNBcuQ7617UfB5qIyBnkig31TmQ==", + "license": "MIT", + "dependencies": { + "@starknet-io/types-js": "^0.7.10", + "@wallet-standard/base": "^1.1.0", + "@wallet-standard/features": "^1.1.0", + "ox": "^0.4.4" + } + }, "node_modules/@starknet-io/starknet-types-08": { "name": "@starknet-io/types-js", "version": "0.8.4", @@ -4883,6 +4986,12 @@ "integrity": "sha512-vXvzENdSe0lvTT2tSdU4hjc5vfVx1BrSFAXcTDhtnArnmGup/Fuei/zb8kKEJ1SqT7AwtdF7/uQ65FP+B4APIA==", "license": "MIT" }, + "node_modules/@starknet-io/types-js": { + "version": "0.7.10", + "resolved": "https://registry.npmjs.org/@starknet-io/types-js/-/types-js-0.7.10.tgz", + "integrity": "sha512-1VtCqX4AHWJlRRSYGSn+4X1mqolI1Tdq62IwzoU2vUuEE72S1OlEeGhpvd6XsdqXcfHmVzYfj8k1XtKBQqwo9w==", + "license": "MIT" + }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -5342,6 +5451,27 @@ "dev": true, "license": "ISC" }, + "node_modules/@wallet-standard/base": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/base/-/base-1.1.0.tgz", + "integrity": "sha512-DJDQhjKmSNVLKWItoKThJS+CsJQjR9AOBOirBVT1F9YpRyC9oYHE+ZnSf8y8bxUphtKqdQMPVQ2mHohYdRvDVQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=16" + } + }, + "node_modules/@wallet-standard/features": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@wallet-standard/features/-/features-1.1.0.tgz", + "integrity": "sha512-hiEivWNztx73s+7iLxsuD1sOJ28xtRix58W7Xnz4XzzA/pF0+aicnWgjOdA10doVDEDZdUuZCIIqG96SFNlDUg==", + "license": "Apache-2.0", + "dependencies": { + "@wallet-standard/base": "^1.1.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/abab": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", @@ -5379,6 +5509,27 @@ "node": ">=12" } }, + "node_modules/abitype": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/abitype/-/abitype-1.0.8.tgz", + "integrity": "sha512-ZeiI6h3GnW06uYDLx0etQtX/p8E24UaHHBj57RSjK7YBFe7iuVn07EDpOeP451D06sF27VOz9JJPlIKJmXgkEg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/wevm" + }, + "peerDependencies": { + "typescript": ">=5.0.4", + "zod": "^3 >=3.22.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/acorn": { "version": "8.15.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", @@ -8350,7 +8501,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true, "license": "MIT" }, "node_modules/execa": { @@ -15785,6 +15935,35 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ox": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ox/-/ox-0.4.4.tgz", + "integrity": "sha512-oJPEeCDs9iNiPs6J0rTx+Y0KGeCGyCAA3zo94yZhm8G5WpOxrwUtn2Ie/Y8IyARSqqY/j9JTKA3Fc1xs1DvFnw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/wevm" + } + ], + "license": "MIT", + "dependencies": { + "@adraffy/ens-normalize": "^1.10.1", + "@noble/curves": "^1.6.0", + "@noble/hashes": "^1.5.0", + "@scure/bip32": "^1.5.0", + "@scure/bip39": "^1.4.0", + "abitype": "^1.0.6", + "eventemitter3": "5.0.1" + }, + "peerDependencies": { + "typescript": ">=5.4.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/p-each-series": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz", @@ -19185,7 +19364,7 @@ "version": "5.7.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/package.json b/package.json index 2298250db..004da5630 100644 --- a/package.json +++ b/package.json @@ -103,6 +103,7 @@ "@noble/hashes": "1.6.0", "@scure/base": "1.2.1", "@scure/starknet": "1.1.0", + "@starknet-io/get-starknet-wallet-standard": "^5.0.0-beta.0", "abi-wan-kanabi": "2.2.4", "lossless-json": "^4.0.1", "pako": "^2.0.4", diff --git a/src/index.ts b/src/index.ts index 3f8e1af35..d7dbc7e97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,6 +49,7 @@ export * from './utils/contract'; export * from './utils/transactionReceipt/transactionReceipt'; export * from './utils/units'; export * as wallet from './wallet/connect'; +export * as walletV5 from './wallet/connectV5'; export * from './global/config'; export * from './global/logger'; export * from './global/logger.type'; diff --git a/src/utils/backward.ts b/src/utils/backward.ts index 4274808d6..41ab6220a 100644 --- a/src/utils/backward.ts +++ b/src/utils/backward.ts @@ -15,7 +15,7 @@ import type { ProviderOrAccount, } from '../types'; import { WalletAccount } from '../wallet'; -import type { StarknetWalletProvider, WalletAccountOptions } from '../wallet/types/index.type'; +import type { StarknetWalletProvider, WalletAccountV4Options } from '../wallet/types/index.type'; /** * Backward compatibility method to create Contract instances using the old arguments-based API @@ -120,7 +120,7 @@ export function createAccount( * ``` */ export function createWalletAccount( - provider: WalletAccountOptions['provider'], + provider: WalletAccountV4Options['provider'], walletProvider: StarknetWalletProvider, address: string, cairoVersion?: CairoVersion, diff --git a/src/wallet/account.ts b/src/wallet/account.ts index f9b8d3e12..d2e2ab2e3 100644 --- a/src/wallet/account.ts +++ b/src/wallet/account.ts @@ -26,7 +26,7 @@ import { switchStarknetChain, watchAsset, } from './connect'; -import type { StarknetWalletProvider, WalletAccountOptions } from './types/index.type'; +import type { StarknetWalletProvider, WalletAccountV4Options } from './types/index.type'; import type { PaymasterOptions } from '../paymaster/types/index.type'; import type { PaymasterInterface } from '../paymaster'; import { @@ -41,7 +41,7 @@ import { export class WalletAccount extends Account implements AccountInterface { public walletProvider: StarknetWalletProvider; - constructor(options: WalletAccountOptions) { + constructor(options: WalletAccountV4Options) { super({ ...options, signer: '' }); // At this point unknown address this.walletProvider = options.walletProvider; diff --git a/src/wallet/accountV5.ts b/src/wallet/accountV5.ts new file mode 100644 index 000000000..88c705730 --- /dev/null +++ b/src/wallet/accountV5.ts @@ -0,0 +1,184 @@ +import type { + AddStarknetChainParameters, + Signature, + WatchAssetParameters, +} from '@starknet-io/starknet-types-08'; + +import type { WalletWithStarknetFeatures } from '@starknet-io/get-starknet-wallet-standard/features'; +import type { StandardEventsChangeProperties } from '@wallet-standard/features'; + +import { Account, AccountInterface } from '../account'; +import { StarknetChainId } from '../global/constants'; +import { ProviderInterface } from '../provider'; +import { + AllowArray, + CairoVersion, + Call, + CompiledSierra, + DeclareContractPayload, + MultiDeployContractResponse, + TypedData, + UniversalDeployerContractPayload, + type PaymasterOptions, +} from '../types'; +import { extractContractHashes } from '../utils/contract'; +import { stringify } from '../utils/json'; +import { + addDeclareTransaction, + addInvokeTransaction, + addStarknetChain, + getPermissions, + subscribeWalletEvent, + requestAccounts, + signMessage, + switchStarknetChain, + watchAsset, +} from './connectV5'; +import type { WalletAccountV5Options } from './types/index.type'; +import type { PaymasterInterface } from '../paymaster'; +import { defaultDeployer } from '../deployer'; + +/** + * WalletAccountV5 class. + * This class is used to create a wallet account that can be used to interact with a Starknet wallet browser extension, using get-starknet v5. + */ +export class WalletAccountV5 extends Account implements AccountInterface { + public walletProvider: WalletWithStarknetFeatures; + + /** + * The function to use to unsubscribe from the wallet events. + * To call before the instance is deleted. + */ + private unsubscribe: () => void; + + constructor(options: WalletAccountV5Options) { + super({ ...options, signer: '' }); // At this point unknown address + this.walletProvider = options.walletProvider; + + // Update Address/network on change + this.unsubscribe = this.walletProvider.features['standard:events'].on( + 'change', + (change: StandardEventsChangeProperties) => { + if (!change.accounts?.length) return; + if (change.accounts[0].address) this.address = change.accounts[0].address; + if (change.accounts[0].chains) + this.channel.setChainId(change.accounts[0].chains[0].slice(9) as StarknetChainId); + } + ); + } + + /** + * WALLET EVENTS + */ + public onChange(callback: (change: StandardEventsChangeProperties) => void): void { + subscribeWalletEvent(this.walletProvider, callback); + } + + public unsubscribeChange(): void { + this.unsubscribe(); + } + + /** + * WALLET SPECIFIC METHODS + */ + public requestAccounts(silentMode = false) { + return requestAccounts(this.walletProvider, silentMode); + } + + public getPermissions() { + return getPermissions(this.walletProvider); + } + + public switchStarknetChain(chainId: StarknetChainId) { + return switchStarknetChain(this.walletProvider, chainId); + } + + public watchAsset(asset: WatchAssetParameters) { + return watchAsset(this.walletProvider, asset); + } + + public addStarknetChain(chain: AddStarknetChainParameters) { + return addStarknetChain(this.walletProvider, chain); + } + + /** + * ACCOUNT METHODS + */ + override execute(calls: AllowArray) { + const txCalls = [].concat(calls as any).map((it) => { + const { contractAddress, entrypoint, calldata } = it; + return { + contract_address: contractAddress, + entry_point: entrypoint, + calldata, + }; + }); + + const params = { + calls: txCalls, + }; + + return addInvokeTransaction(this.walletProvider, params); + } + + override declare(payload: DeclareContractPayload) { + const declareContractPayload = extractContractHashes(payload); + // DISCUSS: HOTFIX: Adapt Abi format + const pContract = payload.contract as CompiledSierra; + const cairo1Contract = { + ...pContract, + abi: stringify(pContract.abi), + }; + if (!declareContractPayload.compiledClassHash) { + throw Error('compiledClassHash is required'); + } + const params = { + compiled_class_hash: declareContractPayload.compiledClassHash, + contract_class: cairo1Contract, + }; + return addDeclareTransaction(this.walletProvider, params); + } + + override async deploy( + payload: UniversalDeployerContractPayload | UniversalDeployerContractPayload[] + ): Promise { + const { calls, addresses } = defaultDeployer.buildDeployerCall(payload, this.address); + const invokeResponse = await this.execute(calls); + return { + ...invokeResponse, + contract_address: addresses, + }; + } + + override signMessage(typedData: TypedData): Promise { + return signMessage(this.walletProvider, typedData); + } + + static async connect( + provider: ProviderInterface, + walletProvider: WalletWithStarknetFeatures, + cairoVersion?: CairoVersion, + paymaster?: PaymasterOptions | PaymasterInterface, + silentMode: boolean = false + ) { + const [accountAddress] = await requestAccounts(walletProvider, silentMode); + return new WalletAccountV5({ + provider, + walletProvider, + address: accountAddress, + cairoVersion, + paymaster, + }); + } + + static async connectSilent( + provider: ProviderInterface, + walletProvider: WalletWithStarknetFeatures, + cairoVersion?: CairoVersion, + paymaster?: PaymasterOptions | PaymasterInterface + ) { + return WalletAccountV5.connect(provider, walletProvider, cairoVersion, paymaster, true); + } + + // TODO: MISSING ESTIMATES +} diff --git a/src/wallet/connectV5.ts b/src/wallet/connectV5.ts new file mode 100644 index 000000000..a2abd46c9 --- /dev/null +++ b/src/wallet/connectV5.ts @@ -0,0 +1,181 @@ +import type { WalletWithStarknetFeatures } from '@starknet-io/get-starknet-wallet-standard/features'; +import type { StandardEventsChangeProperties } from '@wallet-standard/features'; +import { + type WatchAssetParameters, + type AddDeclareTransactionParameters, + type AddInvokeTransactionParameters, + type AddStarknetChainParameters, + type ChainId, + type TypedData, + type Permission, + type Address, + AddInvokeTransactionResult, + AddDeclareTransactionResult, + AccountDeploymentData, + Signature, + SpecVersion, +} from '@starknet-io/starknet-types-08'; + +/** + * Request Permission for wallet account, return addresses that are allowed by user + * @param {WalletWithStarknetFeatures} walletWSF - The get-starknet V5 wallet object to use. + * @param {boolean} [silent_mode=false] false: request user interaction allowance. true: return only pre-allowed + * @returns {Address[]} allowed accounts addresses + */ +export function requestAccounts( + walletWSF: WalletWithStarknetFeatures, + silent_mode: boolean = false +): Promise { + return walletWSF.features['starknet:walletApi'].request({ + type: 'wallet_requestAccounts', + params: { silent_mode }, + }); +} + +/** + * Request if DAPP is connected to wallet. + * @param {WalletWithStarknetFeatures} walletWSF - The get-starknet V5 wallet object to use. + * @returns {Permission[]} "accounts" if permission granted + */ +export function getPermissions(walletWSF: WalletWithStarknetFeatures): Promise { + return walletWSF.features['starknet:walletApi'].request({ type: 'wallet_getPermissions' }); +} + +/** + * Request adding an ERC20 Token to the Wallet List + * @param {WalletWithStarknetFeatures} walletWSF - The get-starknet V5 wallet object to use. + * @param {WatchAssetParameters} asset description of the token to add. + * @returns {boolean} true if the token was added successfully + */ +export function watchAsset( + walletWSF: WalletWithStarknetFeatures, + asset: WatchAssetParameters +): Promise { + return walletWSF.features['starknet:walletApi'].request({ + type: 'wallet_watchAsset', + params: asset, + }); +} + +/** + * Request adding custom Starknet chain + * @param {WalletWithStarknetFeatures} walletWSF - The get-starknet V5 wallet object to use. + * @param {AddStarknetChainParameters} chain description of the chain to add. + * @returns {boolean} true if the chain was added successfully + */ +export function addStarknetChain( + walletWSF: WalletWithStarknetFeatures, + chain: AddStarknetChainParameters +): Promise { + return walletWSF.features['starknet:walletApi'].request({ + type: 'wallet_addStarknetChain', + params: chain, + }); +} + +/** + * Request Wallet Network change + * @param {WalletWithStarknetFeatures} walletWSF - The get-starknet V5 wallet object to use. + * @param {ChainId} chainId encoded name of the chain requested. + * @returns {boolean} true if the chain was changed successfully + */ +export function switchStarknetChain( + walletWSF: WalletWithStarknetFeatures, + chainId: ChainId +): Promise { + return walletWSF.features['starknet:walletApi'].request({ + type: 'wallet_switchStarknetChain', + params: { chainId }, + }); +} + +/** + * Request the current chain ID from the wallet. + * @param {WalletWithStarknetFeatures} walletWSF - The get-starknet V5 wallet object to use. + * @returns {ChainId} The current Starknet chain ID. + */ +export function requestChainId(walletWSF: WalletWithStarknetFeatures): Promise { + return walletWSF.features['starknet:walletApi'].request({ type: 'wallet_requestChainId' }); +} + +/** + * Get deployment data for a contract. + * @param {WalletWithStarknetFeatures} walletWSF - The get-starknet V5 wallet object to use. + * @returns {AccountDeploymentData} The deployment data result. + */ +export function deploymentData( + walletWSF: WalletWithStarknetFeatures +): Promise { + return walletWSF.features['starknet:walletApi'].request({ type: 'wallet_deploymentData' }); +} + +/** + * Add an invoke transaction to the wallet. + * @param {WalletWithStarknetFeatures} walletWSF - The get-starknet V5 wallet object to use. + * @param {AddInvokeTransactionParameters} params The parameters required for the invoke transaction. + * @returns {AddInvokeTransactionResult} The result of adding the invoke transaction. + */ +export function addInvokeTransaction( + walletWSF: WalletWithStarknetFeatures, + params: AddInvokeTransactionParameters +): Promise { + return walletWSF.features['starknet:walletApi'].request({ + type: 'wallet_addInvokeTransaction', + params, + }); +} + +/** + * Add a declare transaction to the wallet. + * @param {WalletWithStarknetFeatures} walletWSF - The get-starknet V5 wallet object to use. + * @param {AddDeclareTransactionParameters} params The parameters required for the declare transaction. + * @returns {AddDeclareTransactionResult} The result of adding the declare transaction. + */ +export function addDeclareTransaction( + walletWSF: WalletWithStarknetFeatures, + params: AddDeclareTransactionParameters +): Promise { + return walletWSF.features['starknet:walletApi'].request({ + type: 'wallet_addDeclareTransaction', + params, + }); +} + +/** + * Sign typed data using the wallet. + * @param {WalletWithStarknetFeatures} walletWSF - The get-starknet V5 wallet object to use. + * @param {TypedData} typedData The typed data to sign. + * @returns {Signature} An array of signatures as strings. + */ +export function signMessage( + walletWSF: WalletWithStarknetFeatures, + typedData: TypedData +): Promise { + return walletWSF.features['starknet:walletApi'].request({ + type: 'wallet_signTypedData', + params: typedData, + }); +} + +/** + * Get the list of supported Wallet API specifications. + * @param {WalletWithStarknetFeatures} walletWSF - The get-starknet V5 wallet object to use. + * @returns {SpecVersion[]} An array of wallet API supported specification strings. + */ +export function supportedSpecs(walletWSF: WalletWithStarknetFeatures): Promise { + return walletWSF.features['starknet:walletApi'].request({ type: 'wallet_supportedSpecs' }); +} + +/** + * Attaches an event handler function for the changes of network and account. + * When the account/network are changed, the specified callback function will be called. + * @param {WalletWithStarknetFeatures} walletWSF - The get-starknet V5 wallet object to use. + * @param {StandardEventsChangeProperties} callback - The function to be called when the account/network are changed. + * @returns {() => void} function to execute to unsubscribe events. + */ +export function subscribeWalletEvent( + walletWSF: WalletWithStarknetFeatures, + callback: (change: StandardEventsChangeProperties) => void +): () => void { + return walletWSF.features['standard:events'].on('change', callback); +} diff --git a/src/wallet/index.ts b/src/wallet/index.ts index 362a768e5..7c4da69bf 100644 --- a/src/wallet/index.ts +++ b/src/wallet/index.ts @@ -1 +1,2 @@ export * from './account'; +export * from './accountV5'; diff --git a/src/wallet/types/index.type.ts b/src/wallet/types/index.type.ts index 71bcbb5b1..a76b58a00 100644 --- a/src/wallet/types/index.type.ts +++ b/src/wallet/types/index.type.ts @@ -1,3 +1,4 @@ +import type { WalletWithStarknetFeatures } from '@starknet-io/get-starknet-wallet-standard/features'; import type { PaymasterInterface } from '../../paymaster'; import type { ProviderInterface } from '../../provider'; import type { CairoVersion, PaymasterOptions, ProviderOptions } from '../../types'; @@ -9,10 +10,18 @@ export type RpcCall = Omit; // This is provider object expected by WalletAccount to communicate with wallet export interface StarknetWalletProvider extends StarknetWindowObject {} -export type WalletAccountOptions = { +export type WalletAccountV4Options = { provider: ProviderOptions | ProviderInterface; walletProvider: StarknetWalletProvider; address: string; cairoVersion?: CairoVersion; paymaster?: PaymasterOptions | PaymasterInterface; }; + +export type WalletAccountV5Options = { + provider: ProviderOptions | ProviderInterface; + walletProvider: WalletWithStarknetFeatures; + address: string; + cairoVersion?: CairoVersion; + paymaster?: PaymasterOptions | PaymasterInterface; +}; diff --git a/www/docs/guides/account/pictures/WalletAccountArchitecture.png b/www/docs/guides/account/pictures/WalletAccountArchitecture.png index 0bc717d5a..bbe52e10f 100644 Binary files a/www/docs/guides/account/pictures/WalletAccountArchitecture.png and b/www/docs/guides/account/pictures/WalletAccountArchitecture.png differ diff --git a/www/docs/guides/account/walletAccount.md b/www/docs/guides/account/walletAccount.md index d9ae8564e..4ded73979 100644 --- a/www/docs/guides/account/walletAccount.md +++ b/www/docs/guides/account/walletAccount.md @@ -6,9 +6,9 @@ sidebar_position: 6 **Use wallets to sign transactions in your DAPP.** -The [`WalletAccount`](../API/classes/WalletAccount) class is similar to the regular [`Account`](../API/classes/Account) class, with the added ability to ask a browser wallet to sign and send transactions. Some other cool functionalities will be detailed hereunder. +The [`WalletAccount`](../API/classes/WalletAccountV5) class is similar to the regular [`Account`](../API/classes/Account) class, with the added ability to ask a Wallet (such as ArgentX, Braavos, etc...) to sign and send transactions. Some other cool functionalities will be detailed hereunder. -The private key of a `WalletAccount` is held in a browser wallet (such as ArgentX, Braavos, etc.), and any signature is managed by the wallet. With this approach DAPPs don't need to manage the security for any private key. +The private key of a `WalletAccount` is held in the Wallet, so any signature is managed by the wallet. With this approach DAPPs don't need to manage the security for any private key. :::caution This class functions only within the scope of a DAPP. It can't be used in a Node.js script. @@ -16,39 +16,49 @@ This class functions only within the scope of a DAPP. It can't be used in a Node ## Architecture +In your DAPP, you have to use the `get-starknet` library to select and interact with a wallet. + ![](./pictures/WalletAccountArchitecture.png) -When retrieving information from Starknet, a `WalletAccount` instance will read directly from the blockchain. That is why at the initialization of a `WalletAccount` a [`Provider`](../API/classes/Provider) instance is a required parameter, it will be used for all reading activities. +`get-starknet v4` is working only in desktop browsers. +`get-starknet v5` is working also in mobile browsers. +You can use the `WalletAccount` class with `get-starknet v4`, and the `WalletAccountV5` class with `get-starknet v5`. -If you want to write to Starknet the `WalletAccount` will ask the browser wallet to sign and send the transaction using the Starknet Wallet API to communicate. +## With get-starknet v5 -As several wallets can be installed in your browser, the `WalletAccount` needs the ID of one of the available wallets. You can ask `get-starknet` to display a list of available wallets and to provide as a response the identifier of the selected wallet, called a `Starknet Windows Object` (referred to as SWO in the rest of this guide). +When retrieving information from Starknet, a `WalletAccountV5` instance will read directly from the blockchain. That is why at the initialization of a `WalletAccountV5` a [`RpcProvider`](../API/classes/ProviderInterface) instance is a required parameter, it will be used for all reading activities. -## Select a Wallet +If you want to write to Starknet the `WalletAccountV5` will ask the wallet to sign and send the transaction using the Starknet Wallet API to communicate. -You can ask the `get-starknet` v4 library to display a list of wallets, then it will ask you to make a choice. It will return the SWO of the wallet the user selected. +As several wallets can be installed in your desktop/mobile, the `WalletAccountV5` needs the ID of one of the available wallets. You can ask `get-starknet v5` to provide a list of available wallets, and you have to select one of them, called a `WalletWithStarknetFeatures` Object. -Using the `get-starknet-core` v4 library you can create your own UI and logic to select the wallet. An example of DAPP using a custom UI: [**here**](https://github.com/PhilippeR26/Starknet-WalletAccount/blob/main/src/app/components/client/WalletHandle/SelectWallet.tsx), in the example you can select only the wallets compatible with the Starknet Wallet API. -![](./pictures/SelectWallet.png) +### Select a Wallet -Instantiating a new `WalletAccount`: +Using the `get-starknet/discovery v5` library you have to create your own UI and logic to select one of the available wallets. An example in a DAPP: [**here**](). In this example you can select only the wallets compatible with the Starknet Wallet API. +![](./pictures/SelectWalletV5.png) + +Instantiating a new `WalletAccountV5`: ```typescript -import { connect } from '@starknet-io/get-starknet'; // v4.0.3 min -import { WalletAccount, wallet } from 'starknet'; // v7.0.1 min -const myFrontendProviderUrl = 'https://starknet-sepolia.public.blastapi.io/rpc/v0_8'; -// standard UI to select a wallet: -const selectedWalletSWO = await connect({ modalMode: 'alwaysAsk', modalTheme: 'light' }); -const myWalletAccount = await WalletAccount.connect( +import { createStore, type Store } from '@starknet-io/get-starknet/discovery'; // v5.0.0 min +import { type WalletWithStarknetFeatures } from '@starknet-io/get-starknet/standard/features'; +import { WalletAccountV5, walletV5 } from 'starknet'; // v7.2.0 min +const myFrontendProviderUrl = 'https://free-rpc.nethermind.io/sepolia-juno/v0_8'; +const store: Store = createStore(); +const walletsList: WalletWithStarknetFeatures[] = store.getWallets(); +// Create you own Component to select one of these wallets. +// Hereunder, selection of 2nd wallet of the list. +const selectedWallet: WalletWithStarknetFeatures = walletsList[1]; +const myWalletAccount: WalletAccountV5 = await WalletAccountV5.connect( { nodeUrl: myFrontendProviderUrl }, - selectedWalletSWO + selectedWallet ); ``` The wallet is connected to this blockchain to write in Starknet: ```typescript -const writeChainId = await wallet.requestChainId(myWalletAccount.walletProvider); +const writeChainId = await walletV5.requestChainId(myWalletAccount.walletProvider); ``` and to this blockchain to read Starknet: @@ -57,44 +67,77 @@ and to this blockchain to read Starknet: const readChainId = await myWalletAccount.getChainId(); ``` -## Use as an Account +### Subscription to events + +You can subscribe to one event with `get-starknet v5`: -Once a new `WalletAccount` is created, you can use all the power of Starknet.js, exactly as a with a normal `Account` instance, for example `myWalletAccount.execute(call)` or `myWalletAccount.signMessage(typedMessage)`: +`onChange`: Triggered each time you change the current account or the current network in the wallet. ```typescript -const claimCall = airdropContract.populate('claim_airdrop', { - amount: amount, - proof: proof, -}); -const resp = await myWalletAccount.execute(claimCall); +import type { StandardEventsChangeProperties } from "@wallet-standard/features"; +const addEvent = useCallback((change: StandardEventsChangeProperties) => { + console.log("Event detected", change.accounts); + if (change.accounts?.length) { + console.log("account event=", change.accounts[0].address); + setCurrentAccount(change.accounts[0].address); + console.log("network event=", change.accounts[0].chains[0]); + setCurrentChainId(change.accounts[0].chains[0].slice(9)); + } +}, []); +... +useEffect(() => { + console.log("Subscribe events..."); + selectedWalletAccountV5?.onChange(addEvent); + return () => { + console.log("Unsubscribe to events..."); + selectedWalletAccountV5?.unsubscribeChange(); +} +} +, [selectedWalletAccountV5, addEvent]); ``` -![](./pictures/executeTx.png) +## With get-starknet v4 -## Use in a Contract instance +The concept of Starknet reading/writing is the same when using `get-starknet v4` and the `WalletAccount` class. -You can connect a `WalletAccount` with a [`Contract`](../API/classes/Contract) instance. All reading actions are performed by the provider of the `WalletAccount`, and all writing actions (that need a signature) are performed by the browser wallet. +### Select a Wallet + +You can ask the `get-starknet v4` library to display a window with a list of wallets, then it will ask you to make a choice. It will return the `StarknetWindowObject` Object (referred to as SWO hereunder) of the wallet the user selected. +![](./pictures/SelectWalletV4.png) + +Instantiating a new `WalletAccount`: ```typescript -const lendContract = new Contract(contract.abi, contractAddress, myWalletAccount); -const qty = await lendContract.get_available_asset(addr); // use of the WalletAccount provider -const resp = await lendContract.process_lend_asset(addr); // use of the browser wallet +import { connect } from '@starknet-io/get-starknet'; // v4.0.3 min +import { WalletAccount, wallet } from 'starknet'; // v7.0.1 min +const myFrontendProviderUrl = 'https://starknet-sepolia.public.blastapi.io/rpc/v0_8'; +// standard UI to select a wallet: +const selectedWalletSWO = await connect({ modalMode: 'alwaysAsk', modalTheme: 'light' }); +const myWalletAccount = await WalletAccount.connect( + { nodeUrl: myFrontendProviderUrl }, + selectedWalletSWO +); ``` -## Use as a Provider +:::tip +Using the `get-starknet-core` v4 library you can create your own UI and logic to select the wallet. An example of DAPP using a custom UI [**here**](https://github.com/PhilippeR26/Starknet-WalletAccount/blob/53514a5529c4aebe9e7c6331186e83b7a7310ce0/src/app/components/client/WalletHandle/SelectWallet.tsx), in this example you can select only the wallets compatible with the Starknet Wallet API. +::: -Your `WalletAccount` instance can be used as a provider: +The wallet is connected to this blockchain to write in Starknet: ```typescript -const bl = await myWalletAccount.getBlockNumber(); -// bl = 2374543 +const writeChainId = await wallet.requestChainId(myWalletAccount.walletProvider); ``` -You can use all the methods of the `Provider` class. Under the hood, the `WalletAccount` will use the RPC node that you indicated at its instantiation. +and to this blockchain to read Starknet: + +```typescript +const readChainId = await myWalletAccount.getChainId(); +``` -## Subscription to events +### Subscription to events -You can subscribe to 2 events: +You can subscribe to 2 events with `get-starknet v4`: - `accountsChanged`: Triggered each time you change the current account in the wallet. - `networkChanged`: Triggered each time you change the current network in the wallet. @@ -102,9 +145,9 @@ You can subscribe to 2 events: At each change of the network, both account and network events are emitted. At each change of the account, only the account event is emitted. -### Subscribe +#### Subscribe -#### accountsChanged +##### accountsChanged ```typescript const handleAccount: AccountChangeEventHandler = (accounts: string[] | undefined) => { @@ -116,18 +159,18 @@ const handleAccount: AccountChangeEventHandler = (accounts: string[] | undefined selectedWalletSWO.on('accountsChanged', handleAccount); ``` -#### networkChanged +##### networkChanged ```typescript const handleNetwork: NetworkChangeEventHandler = (chainId?: string, accounts?: string[]) => { if (!!chainId) { - setChangedNetwork(chainId); - } // from a React useState + setChangedNetwork(chainId); // from a React useState + } }; selectedWalletSWO.on('networkChanged', handleNetwork); ``` -### Unsubscribe +#### Unsubscribe Similar to subscription, by using the `.off` method. @@ -141,7 +184,44 @@ You can subscribe both with the SWO or with a `WalletAccount` instance. The above examples are using the SWO, because it is the simpler way to process. ::: -## Direct access to the wallet API entry points +## WalletAccount usage + +### Use as an Account + +Once a new `WalletAccount` or `WalletAccountV5` is created, you can use all the power of Starknet.js, exactly as a with a normal `Account` instance, for example `myWalletAccount.execute(call)` or `myWalletAccount.signMessage(typedMessage)`: + +```typescript +const claimCall = airdropContract.populate('claim_airdrop', { + amount: amount, + proof: proof, +}); +const resp = await myWalletAccount.execute(claimCall); +``` + +![](./pictures/executeTx.png) + +### Use in a Contract instance + +You can connect a `WalletAccount` with a [`Contract`](../API/classes/Contract) instance. All reading actions are performed by the provider of the `WalletAccount`, and all writing actions (that need a signature) are performed by the wallet. + +```typescript +const lendContract = new Contract(contract.abi, contractAddress, myWalletAccount); +const qty = await lendContract.get_available_asset(addr); // use of the WalletAccount provider +const resp = await lendContract.process_lend_asset(addr); // use of the wallet +``` + +### Use as a Provider + +Your `WalletAccount` instance can be used as a provider: + +```typescript +const bl = await myWalletAccount.getBlockNumber(); +// bl = 2374543 +``` + +You can use all the methods of the `RpcProvider` class. Under the hood, the `WalletAccount` will use the RPC node that you indicated at its instantiation. + +### Direct access to the wallet API entry points The `WalletAccount` class is able to interact with all the entrypoints of the Starknet Wallet API, including some functionalities that do not exists in the `Account` class. @@ -149,7 +229,7 @@ A full description of this API can be found [**here**](https://github.com/starkn Some examples: -### Request to change the wallet network +#### Request to change the wallet network Using your `WalletAccount`, you can ask the wallet to change its current network: @@ -169,7 +249,7 @@ useEffect( ![](./pictures/switchNetwork.png) -### Request to display a token in the wallet +#### Request to display a token in the wallet Using your `WalletAccount`, you can ask the wallet to display a new token: @@ -194,7 +274,7 @@ useEffect( ![](./pictures/addToken.png) -## Changing the network or account +### Changing the network or account When you change the network or the account address a `WalletAccount` instance is automatically updated, however, this can lead to unexpected behavior if one is not careful (reads and writes targeting different networks, problems with Cairo versions of the accounts, ...). diff --git a/www/docs/guides/pictures/SelectWalletV4.png b/www/docs/guides/pictures/SelectWalletV4.png new file mode 100644 index 000000000..79a6726a8 Binary files /dev/null and b/www/docs/guides/pictures/SelectWalletV4.png differ diff --git a/www/docs/guides/pictures/SelectWalletV5.png b/www/docs/guides/pictures/SelectWalletV5.png new file mode 100644 index 000000000..0243346f7 Binary files /dev/null and b/www/docs/guides/pictures/SelectWalletV5.png differ