diff --git a/.prettierrc.js b/.prettierrc.cjs similarity index 100% rename from .prettierrc.js rename to .prettierrc.cjs diff --git a/package.json b/package.json index 1cdb91d..3db9520 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "eslint-plugin-promise": "^6.1.1", "prettier": "^2.7.1", "prettier-plugin-packagejson": "^2.2.18", + "prettier-plugin-svelte": "^3.2.4", "sharp": "^0.32.6", "typescript": "^4.7.4", "typescript-eslint": "^8.0.0-alpha.20" diff --git a/packages/site/src/components/CreateAccount.svelte b/packages/site/src/components/CreateAccount.svelte new file mode 100644 index 0000000..8fe5d70 --- /dev/null +++ b/packages/site/src/components/CreateAccount.svelte @@ -0,0 +1,73 @@ + + +

Create Jungle4 Account

+
+
+ + +
+
+ + +
+ +
diff --git a/packages/site/src/lib/account.ts b/packages/site/src/lib/account.ts index 007ab98..8ffdecc 100644 --- a/packages/site/src/lib/account.ts +++ b/packages/site/src/lib/account.ts @@ -1,3 +1,7 @@ import { writable } from 'svelte/store'; export const accountName = writable(null); + +export const accountPermission = writable(null); + +export const accountPublicKey = writable(null); diff --git a/packages/site/src/lib/rpc-methods.ts b/packages/site/src/lib/rpc-methods.ts new file mode 100644 index 0000000..79d6fe3 --- /dev/null +++ b/packages/site/src/lib/rpc-methods.ts @@ -0,0 +1,21 @@ +import { accountName } from './account'; +import { invokeSnap } from './snap'; + +export async function connectAccount() { + const result = await invokeSnap({ method: 'eos_connectAccount' }); + console.log('accountName', result); + accountName.set(result); +} + +export async function getConnectedAccount() { + const account = await invokeSnap({ method: 'eos_getConnectedAccount' }); + console.log('account', account); + accountName.set(account); + // accountPublicKey.set(account.publicKey); + // accountPermission.set(account.permission); +} + +export async function testTransaction() { + const result = await invokeSnap({ method: 'eos_signTransaction' }); + console.log('result', result); +} diff --git a/packages/site/src/routes/+page.svelte b/packages/site/src/routes/+page.svelte index 86ce700..77980d9 100644 --- a/packages/site/src/routes/+page.svelte +++ b/packages/site/src/routes/+page.svelte @@ -6,6 +6,8 @@ import { setSnap, requestSnap, invokeSnap } from '$lib/snap'; import flask_fox from '../assets/flask_fox.svg'; import { accountName } from '$lib/account'; + import CreateAccount from '../components/CreateAccount.svelte'; + import { connectAccount, getConnectedAccount, testTransaction } from '$lib/rpc-methods'; // import type {RpcMethodTypes} from '@greymass/eos-snap'; let provider: MetaMaskInpageProvider; @@ -19,76 +21,9 @@ provider = $snapProvider; // gotta be a better way of narrowing this type isFlask.set(await checkIsFlask(provider)); setSnap(); + getConnectedAccount(); } }); - - async function connectAccount() { - const result = await invokeSnap({ method: 'eos_connectAccount' }); - console.log('accountName', result); - accountName.set(result); - } - - let account = { name: '', publicKey: '' }; - - type AccountData = { - accountName: string; - activeKey: string; - ownerKey: string; - chainId: string; - }; - - async function handleFormSubmit(event: Event) { - const formData = new FormData(event.target as HTMLFormElement); - - const accountData: AccountData = { - accountName: formData.get('account') as string, - activeKey: formData.get('publicKey') as string, - ownerKey: formData.get('publicKey') as string, - chainId: '73e4385a2708e6d7048834fbc1079f2fabb17b3c125b146af438971e90716c4d' - }; - - const name = await createAccount(accountData, 'https://jungle4.greymass.com'); - - if (typeof name !== 'undefined') { - console.log(`Account ${name} created`); - connectAccount(); - } - } - - async function createAccount(accountData: AccountData, chainUrl: string) { - const data = { - accountName: accountData.accountName, - activeKey: accountData.activeKey, - ownerKey: accountData.ownerKey, - network: accountData.chainId - }; - - try { - const response = await fetch(`${chainUrl}/account/create`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(data) - }); - - console.log(response); - - if (response.status === 201) { - console.log('success:', JSON.stringify(await response.text(), null, 2)); - return accountData.accountName; - } - - console.log('failure:', JSON.stringify(await response.text(), null, 2)); - } catch (error) { - console.error('error getting response', error); - } - } - - async function testTransaction() { - const result = await invokeSnap({ method: 'eos_signTransaction' }); - console.log('result', result); - }

@@ -101,27 +36,20 @@

Is flask: {$isFlask}

Is metamask ready: {$isMetaMaskReady}

Is snap installed: {isSnapInstalled}

+ +

The snap will need to be re-installed after any changes to the code.

+

We disable the connection button when an account is already connected.

+
-

Create Jungle4 Account

-
-
- - -
-
- - -
- -
+ diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index 5f1bab2..0f5bd94 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -7,7 +7,7 @@ "url": "https://github.com/MetaMask/template-snap-monorepo.git" }, "source": { - "shasum": "T9QRYWyyVoAGWQZTG3ByknNkPuBK8CI6OQxkQ96gg5I=", + "shasum": "jyxS5srKby/XVNf3aoW+WrKPwt1e6NPo9aRhJyaY+E8=", "location": { "npm": { "filePath": "dist/bundle.js", @@ -17,6 +17,7 @@ } }, "initialPermissions": { + "snap_manageState": {}, "snap_dialog": {}, "endowment:page-home": {}, "endowment:network-access": {}, diff --git a/packages/snap/src/api/index.ts b/packages/snap/src/api/index.ts index 67d5afb..71d05b0 100644 --- a/packages/snap/src/api/index.ts +++ b/packages/snap/src/api/index.ts @@ -1,4 +1,12 @@ -import { APIClient, Name, PermissionLevel, PublicKey, SignedTransaction, TransactionHeader, UInt32, Weight } from '@wharfkit/antelope'; +import { + APIClient as AntelopeAPIClient, + Name, + PermissionLevel, + PublicKey, + TransactionHeader, + UInt32, + Weight, +} from '@wharfkit/antelope'; import { Account } from '../models'; type NetworkAccount = { @@ -8,51 +16,48 @@ type NetworkAccount = { authorizing_account: PermissionLevel; weight: Weight; threshold: UInt32; -} +}; interface APIGateway { fetchAccounts(publicKey: PublicKey): Promise; fetchAccountByKey(publicKey: PublicKey): Promise; getTransactionHeader(secondsAhead?: number): Promise; - pushTransaction(signedTransaction: SignedTransaction): Promise; } -export class Client implements APIGateway { - client: APIClient - chainUrl: string +export class ApiClient implements APIGateway { + client: AntelopeAPIClient; - constructor(chainUrl: string) { - this.client = new APIClient({url: chainUrl}); - this.chainUrl = chainUrl; + constructor(url: string) { + this.client = new AntelopeAPIClient({ url }); } - private dataToAccount(data: NetworkAccount): Account { + private accountDecoder(data: NetworkAccount): Account { return new Account( String(data.account_name), String(data.permission_name), - String(data.authorizing_key) + String(data.authorizing_key), ); } public async fetchAccounts(publicKey: PublicKey) { - const { accounts } = await this.client.v1.chain.get_accounts_by_authorizers({keys: [publicKey.toString()]}); - return accounts.map(this.dataToAccount); + const { accounts } = await this.client.v1.chain.get_accounts_by_authorizers( + { keys: [publicKey.toString()] }, + ); + return accounts.map(this.accountDecoder); } public async fetchAccountByKey(publicKey: PublicKey) { - const { accounts } = await this.client.v1.chain.get_accounts_by_authorizers({keys: [publicKey.toString()]}); + const { accounts } = await this.client.v1.chain.get_accounts_by_authorizers( + { keys: [publicKey.toString()] }, + ); if (!accounts[0]) return null; const [account] = accounts; - return this.dataToAccount(account); + return this.accountDecoder(account); } public async getTransactionHeader(secondsAhead = 90) { - return this.client.v1.chain.get_info().then(info => info.getTransactionHeader(secondsAhead)); + return this.client.v1.chain + .get_info() + .then((info) => info.getTransactionHeader(secondsAhead)); } - - public async pushTransaction(signedTransaction: SignedTransaction) { - const result = await this.client.v1.chain.push_transaction(signedTransaction); - return String(result.processed); - } - } diff --git a/packages/snap/src/index.ts b/packages/snap/src/index.ts index 92e6d44..5e5be6a 100644 --- a/packages/snap/src/index.ts +++ b/packages/snap/src/index.ts @@ -1,5 +1,9 @@ -import { OnRpcRequestHandler } from '@metamask/snaps-sdk'; -import { signTransaction, connectAccount } from './rpc'; +import { + type OnRpcRequestHandler, + MethodNotFoundError, +} from '@metamask/snaps-sdk'; + +import { signTransaction, connectAccount, getConnectedAccount } from './rpc'; export * from './rpc-types'; @@ -13,9 +17,7 @@ export * from './rpc-types'; * @returns The result of `snap_dialog`. * @throws If the request method is not valid for this snap. */ -export const onRpcRequest: OnRpcRequestHandler = async ({ - request, -}) => { +export const onRpcRequest: OnRpcRequestHandler = async ({ request }) => { switch (request.method) { case 'eos_connectAccount': return await connectAccount(); @@ -23,8 +25,11 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ case 'eos_signTransaction': return await signTransaction(); + case 'eos_getConnectedAccount': + return await getConnectedAccount(); + default: - throw new Error('Method not found.'); + // eslint-disable-next-line @typescript-eslint/no-throw-literal + throw new MethodNotFoundError(request.method); } }; - diff --git a/packages/snap/src/lib/chains.ts b/packages/snap/src/lib/chains.ts new file mode 100644 index 0000000..e200c9b --- /dev/null +++ b/packages/snap/src/lib/chains.ts @@ -0,0 +1,33 @@ +import { Chains } from '@wharfkit/common'; +import { Chain } from '../models'; +import { StateManager } from './manageState'; + +const coinType = { + EOS: 194, + Jungle4: 194, + WAX: 14001, + Telos: 424, +}; + +export const supportedChains = { + EOS: { id: Chains.EOS.id, url: Chains.EOS.url, coinType: coinType.EOS }, + Jungle4: { + id: Chains.Jungle4.id, + url: Chains.Jungle4.url, + coinType: coinType.Jungle4, + }, + WAX: { id: Chains.WAX.id, url: Chains.WAX.url, coinType: coinType.WAX }, + Telos: { + id: Chains.Telos.id, + url: Chains.Telos.url, + coinType: coinType.Telos, + }, +}; + +export type SupportedChain = keyof typeof supportedChains; + +export async function getCurrentChain() { + const state = new StateManager(); + const savedChain = (await state.getValue('currentChain')) as string; + return Chain.from(savedChain); +} diff --git a/packages/snap/src/lib/keyDeriver.ts b/packages/snap/src/lib/keyDeriver.ts index 7f65147..90e3742 100644 --- a/packages/snap/src/lib/keyDeriver.ts +++ b/packages/snap/src/lib/keyDeriver.ts @@ -1,50 +1,45 @@ import { getBIP44AddressKeyDeriver } from '@metamask/key-tree'; -import { - Bytes, - KeyType as AntelopeKeyType, - PrivateKey, - PublicKey -} from '@wharfkit/antelope'; +import * as Antelope from '@wharfkit/antelope'; +import type { Chain } from 'src/models'; /** - * Get the key deriver for EOS. + * Get the key deriver for the given coin type. * * @param coinType - The SLIP-0044 registered coin type for BIP-0044. * @returns The key deriver. */ -async function getKeyDeriver(coinType: number = 194) { - const eosNode = await snap.request({ +async function getKeyDeriver(coinType = 194) { + const networkNode = await snap.request({ method: 'snap_getBip44Entropy', - params: { - coinType, - }, + params: { coinType }, }); - return getBIP44AddressKeyDeriver(eosNode); + return getBIP44AddressKeyDeriver(networkNode); } /** * Derive an Antelope public key from the key tree at the given address index. * - * @param coinType - The SLIP-0044 registered coin type for BIP-0044. + * @param chain - The chain to derive the key for. * @param addressIndex - The index of the address to derive. * @returns The public key. * @throws If the key tree is not initialized. */ -export async function derivePublicKey(coinType?: number, addressIndex = 0) { - const privateKey = await derivePrivateKey(coinType, addressIndex); +export async function derivePublicKey(chain: Chain, addressIndex = 0) { + const privateKey = await derivePrivateKey(chain, addressIndex); return privateKey.toPublic(); } /** * Derive an Antelope private key from the key tree at the given address index. * - * @param coinType - The SLIP-0044 registered coin type for BIP-0044. + * @param chain - The chain to derive the key for. * @param addressIndex - The index of the address to derive. * @returns The private key. * @throws If the key tree is not initialized. */ -export async function derivePrivateKey(coinType?: number, addressIndex = 0) { +export async function derivePrivateKey(chain: Chain, addressIndex = 0) { + const { coinType } = chain; const keyDeriver = await getKeyDeriver(coinType); const derived = await keyDeriver(addressIndex); @@ -52,5 +47,8 @@ export async function derivePrivateKey(coinType?: number, addressIndex = 0) { throw new Error('Private key not found'); } - return new PrivateKey(AntelopeKeyType.K1, Bytes.from(derived.privateKeyBytes)); + return new Antelope.PrivateKey( + Antelope.KeyType.K1, + Antelope.Bytes.from(derived.privateKeyBytes), + ); } diff --git a/packages/snap/src/lib/manageState.ts b/packages/snap/src/lib/manageState.ts new file mode 100644 index 0000000..19223ea --- /dev/null +++ b/packages/snap/src/lib/manageState.ts @@ -0,0 +1,36 @@ +import { assert, type Json } from '@metamask/snaps-sdk'; + +export class StateManager { + public async get() { + const state = await snap.request({ + method: 'snap_manageState', + params: { operation: 'get' }, + }); + return state; + } + + public async getValue(key: string) { + const state = await this.get(); + assert(state, 'State not found'); + assert(state[key], `Key ${key} not found in state`); + return state[key]; + } + + public async set(obj: Record) { + const state = await this.get(); + await snap.request({ + method: 'snap_manageState', + params: { + operation: 'update', + newState: { ...state, ...obj }, + }, + }); + } + + public async clear() { + await snap.request({ + method: 'snap_manageState', + params: { operation: 'clear' }, + }); + } +} diff --git a/packages/snap/src/models.ts b/packages/snap/src/models.ts index 7392b59..f1f10d4 100644 --- a/packages/snap/src/models.ts +++ b/packages/snap/src/models.ts @@ -1,3 +1,40 @@ +import { Checksum256 } from '@wharfkit/antelope'; + export class Account { - constructor(public name: string, public permission: string, public publicKey: string) {} + constructor( + public name: string, + public permission: string, + public publicKey: string, + ) {} +} + +export class Chain { + id: Checksum256; + url: string; + coinType: number; + + constructor({ + id, + url, + coinType, + }: { + id: string; + url: string; + coinType: string; + }) { + this.id = Checksum256.from(id); + this.url = url; + this.coinType = parseInt(coinType, 10); + } + + static from(json: string): Chain { + console.log(json); + const parsed = JSON.parse(json); + console.log(parsed); + return new Chain({ + id: parsed.id, + url: parsed.url, + coinType: parsed.coinType, + }); + } } diff --git a/packages/snap/src/rpc.ts b/packages/snap/src/rpc.ts index 2f3b26a..8d8d622 100644 --- a/packages/snap/src/rpc.ts +++ b/packages/snap/src/rpc.ts @@ -1,28 +1,33 @@ import { assert } from '@metamask/snaps-sdk'; -import { Chains } from '@wharfkit/common'; -import { Session, SignedTransaction } from '@wharfkit/session'; +import { getCurrentChain, SupportedChain, supportedChains } from './lib/chains'; +import { Session } from '@wharfkit/session'; import { WalletPluginPrivateKey } from '@wharfkit/wallet-plugin-privatekey'; -import { Client } from './api'; +import { ApiClient } from './api'; import { derivePrivateKey, derivePublicKey } from './lib/keyDeriver'; +import { StateManager } from './lib/manageState'; import { makeMockTransaction } from './lib/mockTransfer'; -import { userConfirmedAccount, alertNoAccountFound, userConfirmedTransaction } from './ui'; +import { + alertNoAccountFound, + userConfirmedAccount, + userConfirmedTransaction, +} from './ui'; -// There's something that can be done here with the chain and chain ID -// For example, we might want to use the chain ID to determine the chain URL -// and we need to associate that to the coin type for the derivation path - -export async function connectAccount() { - const publicKey = await derivePublicKey(); - const chain = { - id: Chains.Jungle4.id, - url: Chains.Jungle4.url, - } - const api = new Client(chain.url); +export async function connectAccount(chainName: SupportedChain = 'Jungle4') { + const chain = supportedChains[chainName]; + const publicKey = await derivePublicKey(chain); + const api = new ApiClient(chain.url); const account = await api.fetchAccountByKey(publicKey); + console.log(JSON.stringify(account)); + console.log(JSON.stringify(chain)); if (account) { const confirmed = await userConfirmedAccount(account.name); if (!confirmed) return null; + const state = new StateManager(); + await state.set({ + account: JSON.stringify(account), + currentChain: JSON.stringify(chain), + }); return account.name; } else { await alertNoAccountFound(publicKey.toString()); @@ -30,54 +35,57 @@ export async function connectAccount() { } } - - -export async function signTransaction() { // TODO: will need params - const publicKey = await derivePublicKey(); - const chain = { - id: Chains.Jungle4.id, - url: Chains.Jungle4.url, - } - const api = new Client(chain.url); +// TODO: will need params +export async function signTransaction() { + console.log('signTransaction'); + const chain = await getCurrentChain(); + const api = new ApiClient(chain.url); + const publicKey = await derivePublicKey(chain); const account = await api.fetchAccountByKey(publicKey); - assert(account, 'Account not found') - const header = await api.getTransactionHeader(); + + assert(account, 'Account not found'); // Will be replaced with actual transaction data from params - const memo = 'test' + const memo = 'test'; const transferObject = { - from: account.name.toString(), + from: account.name, to: 'teamgreymass', quantity: '0.1337 EOS', memo: memo || 'wharfkit is the best <3', - } + }; - const transaction = makeMockTransaction(account, header, transferObject) // TODO: will need params + const header = await api.getTransactionHeader(); + const transaction = makeMockTransaction(account, header, transferObject); // TODO: will need params + console.log(JSON.stringify(transaction)); - const confirmed = await userConfirmedTransaction(transferObject) + const confirmed = await userConfirmedTransaction(transferObject); if (confirmed) { - const privateKey = await derivePrivateKey(); - assert(privateKey, 'Private key not found') - // const signature = privateKey.signDigest(transaction.signingDigest(chain.id)) - // const signedTransaction = SignedTransaction.from({ - // ...transaction, - // signatures: [signature], - // }) - // const result = await api.pushTransaction(signedTransaction) + const privateKey = await derivePrivateKey(chain); + console.log(privateKey); + assert(privateKey, 'Private key not found'); const sessionArgs = { - chain, + chain: { + id: chain.id, + url: chain.url, + }, actor: account.name, permission: account.permission, - walletPlugin: new WalletPluginPrivateKey(privateKey) - } - const session = new Session(sessionArgs) - const result = await session.transact(transaction) - console.log(JSON.stringify(result)) - return String(result) - + walletPlugin: new WalletPluginPrivateKey(privateKey), + }; + console.log(sessionArgs); + const session = new Session(sessionArgs); + console.log(JSON.stringify(session)); + const result = await session.transact(transaction); + console.log(JSON.stringify(result)); + return String(result); } + return null; +} - return null - +export async function getConnectedAccount() { + const state = new StateManager(); + const account = (await state.getValue('account')) as string; + if (!account) return null; + return account; } diff --git a/yarn.lock b/yarn.lock index 63cd705..81e7a49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10153,7 +10153,7 @@ __metadata: languageName: node linkType: hard -"prettier-plugin-svelte@npm:^3.1.2": +"prettier-plugin-svelte@npm:^3.1.2, prettier-plugin-svelte@npm:^3.2.4": version: 3.2.4 resolution: "prettier-plugin-svelte@npm:3.2.4" peerDependencies: @@ -10804,6 +10804,7 @@ __metadata: eslint-plugin-promise: ^6.1.1 prettier: ^2.7.1 prettier-plugin-packagejson: ^2.2.18 + prettier-plugin-svelte: ^3.2.4 sharp: ^0.32.6 typescript: ^4.7.4 typescript-eslint: ^8.0.0-alpha.20