diff --git a/packages/snap/package.json b/packages/snap/package.json index 71555dc..c8a4ff1 100644 --- a/packages/snap/package.json +++ b/packages/snap/package.json @@ -1,6 +1,6 @@ { "name": "@near-snap/plugin", - "version": "0.6.1", + "version": "0.7.0", "description": "View and sign transactions for NEAR Protocol", "repository": { "type": "git", @@ -47,7 +47,7 @@ "@metamask/eslint-config-jest": "^10.0.0", "@metamask/eslint-config-nodejs": "^10.0.0", "@metamask/eslint-config-typescript": "^10.0.0", - "@metamask/snaps-cli": "^0.32.2", + "@metamask/snaps-cli": "^4.0.0", "@metamask/snaps-jest": "^0.35.2-flask.1", "@types/lodash.merge": "^4.6.7", "@typescript-eslint/eslint-plugin": "^5.33.0", diff --git a/packages/snap/snap.manifest.json b/packages/snap/snap.manifest.json index f49a020..6fd876f 100644 --- a/packages/snap/snap.manifest.json +++ b/packages/snap/snap.manifest.json @@ -1,5 +1,5 @@ { - "version": "0.6.1", + "version": "0.7.0", "description": "Sign transaction for NEAR Protocol. Created by BANYAN and HERE Wallet", "proposedName": "NEAR Protocol", "repository": { @@ -7,7 +7,7 @@ "url": "https://github.com/here-wallet/near-snap.git" }, "source": { - "shasum": "77c7l1DmnhV9Ja1LdYHefYaYESZoFw7BTMO3PX52VX8=", + "shasum": "UdR3ks5jg2wnyNCGlbnGMaySI22DS5ngPL/JYyUv9hE=", "location": { "npm": { "filePath": "dist/bundle.js", diff --git a/packages/snap/src/core/createAction.ts b/packages/snap/src/core/createAction.ts index 5b84843..c28b11b 100644 --- a/packages/snap/src/core/createAction.ts +++ b/packages/snap/src/core/createAction.ts @@ -3,7 +3,11 @@ import { PublicKey } from '@near-js/crypto/lib/public_key'; import * as transactions from 'near-api-js/lib/transaction'; import { ActionJson, AddKeyPermissionJson } from '../interfaces'; -const getAccessKey = (permission: AddKeyPermissionJson) => { +const getAccessKey = ({ + permission, +}: AddKeyPermissionJson): transactions.AccessKey => { + if (permission === 'FullAccess') return transactions.fullAccessKey(); + const { receiverId, methodNames = [] } = permission; const allowance = permission.allowance ? new BN(permission.allowance) @@ -34,7 +38,7 @@ export const createAction = (action: ActionJson): transactions.Action => { const { publicKey, accessKey } = action.params; return transactions.addKey( PublicKey.from(publicKey), - getAccessKey(accessKey.permission), + getAccessKey(accessKey), ); } diff --git a/packages/snap/src/core/getAccount.ts b/packages/snap/src/core/getAccount.ts index 15ad201..81eadc7 100644 --- a/packages/snap/src/core/getAccount.ts +++ b/packages/snap/src/core/getAccount.ts @@ -2,7 +2,7 @@ import { copyable, heading, panel, text } from '@metamask/snaps-ui'; import { JsonBIP44Node } from '@metamask/key-tree'; import { KeyPair } from '@near-js/crypto/lib/key_pair'; import { SnapsGlobalObject } from '@metamask/snaps-types'; -import { InMemoryKeyStore } from 'near-api-js/lib/key_stores'; +import { InMemoryKeyStore } from 'near-api-js/lib/key_stores/in_memory_key_store'; import { InMemorySigner } from 'near-api-js/lib/signer'; import nacl from 'tweetnacl'; import bs58 from 'bs58'; @@ -17,6 +17,8 @@ const nearNetwork = { testnet: 1, }; +export const NICKNAME_KEY = '@nickname'; + export async function getKeyPair( snap: SnapsGlobalObject, network: NearNetwork, @@ -42,11 +44,19 @@ export async function getKeyPair( export async function getSigner(snap: SnapsGlobalObject, network: NearNetwork) { const keyPair = await getKeyPair(snap, network); - const accountId = Buffer.from(keyPair.getPublicKey().data).toString('hex'); + const address = Buffer.from(keyPair.getPublicKey().data).toString('hex'); + + let state: any = await snap.request({ + method: 'snap_manageState', + params: { operation: 'get' }, + }); + + const accountId = state?.[network]?.[NICKNAME_KEY] || address; const keystore = new InMemoryKeyStore(); await keystore.setKey(network, accountId, keyPair); const signer = new InMemorySigner(keystore); + return { signer, publicKey: keyPair.getPublicKey(), accountId }; } diff --git a/packages/snap/src/core/permissions.ts b/packages/snap/src/core/permissions.ts index 7f6a368..bb1c221 100644 --- a/packages/snap/src/core/permissions.ts +++ b/packages/snap/src/core/permissions.ts @@ -2,7 +2,7 @@ import { SnapsGlobalObject } from '@metamask/snaps-types'; import { copyable, divider, heading, panel, text } from '@metamask/snaps-ui'; import { NearNetwork } from '../interfaces'; import { InputAssertError } from './validations'; -import { getSigner } from './getAccount'; +import { NICKNAME_KEY, getSigner } from './getAccount'; import { t } from './locales'; type PermissionsPath = { @@ -16,6 +16,8 @@ type ConnectOptions = { contractId?: string; } & PermissionsPath; +type BindNicknameOptions = { nickname: string } & PermissionsPath; + export async function getPermissions( data: PermissionsPath, ): Promise | null> { @@ -28,6 +30,51 @@ export async function getPermissions( return origin ?? null; } +export async function bindNickname(params: BindNicknameOptions) { + const WHITELIST = [ + 'https://my.herewallet.app', + 'https://beta.herewallet.app', + ]; + + if (WHITELIST.includes(params.origin) === false) { + throw new InputAssertError(t('connectApp.accessDenied')); + } + + let state: any = await snap.request({ + method: 'snap_manageState', + params: { operation: 'get' }, + }); + + if (!state) state = {}; + if (!state[params.network]) state[params.network] = {}; + if (state[params.network][NICKNAME_KEY] != null) { + throw new InputAssertError(t('connectApp.accessDenied')); + } + + const view = panel([text(t('bindNickname.site', params.origin))]); + view.children.push( + heading(t('bindNickname.title')), + text(t('bindNickname.text', params.network)), + text(t('bindNickname.newAddress')), + copyable(params.nickname), + ); + + const isConfirmed = await snap.request({ + method: 'snap_dialog', + params: { type: 'confirmation', content: view }, + }); + + if (!isConfirmed) { + throw new InputAssertError(t('connectApp.accessDenied')); + } + + state[params.network][NICKNAME_KEY] = params.nickname; + await snap.request({ + method: 'snap_manageState', + params: { operation: 'update', newState: state }, + }); +} + export async function disconnectApp(params: PermissionsPath) { let data: any = await snap.request({ method: 'snap_manageState', diff --git a/packages/snap/src/core/validations.ts b/packages/snap/src/core/validations.ts index 4e6f238..d07e446 100644 --- a/packages/snap/src/core/validations.ts +++ b/packages/snap/src/core/validations.ts @@ -1,4 +1,3 @@ -import { PublicKey } from '@near-js/crypto'; import { object, array, @@ -16,17 +15,9 @@ import { union, any, } from 'superstruct'; +import { PublicKey } from 'near-api-js/lib/utils/key_pair'; import { NearNetwork } from '../interfaces'; -export const safeThrowable = (exec: () => void) => { - try { - exec(); - return true; - } catch { - return false; - } -}; - const ACCOUNT_ID_REGEX = /^(([a-z\d]+[-_])*[a-z\d]+\.)*([a-z\d]+[-_])*[a-z\d]+$/u; @@ -41,17 +32,34 @@ export const accountId = () => ); export const url = () => - define('url', (value: any) => safeThrowable(() => new URL(value))); + define('url', (value: any) => { + try { + new URL(value); + return true; + } catch { + return false; + } + }); export const publicKey = () => - define('publicKey', (value: any) => - safeThrowable(() => PublicKey.fromString(value)), - ); + define('publicKey', (value: any) => { + try { + PublicKey.fromString(value); + return true; + } catch (e) { + return false; + } + }); export const serializedBigInt = () => - define('serializedBigInt', (value: any) => - safeThrowable(() => BigInt(value)), - ); + define('serializedBigInt', (value: any) => { + try { + BigInt(value); + return true; + } catch (e) { + return false; + } + }); export const networkSchemaDefaulted: Describe = defaulted( enums(['testnet', 'mainnet']), @@ -80,20 +88,24 @@ export const transferAction = object({ }), }); -export const addKeyPermissionSchema = object({ - receiverId: accountId(), - allowance: optional(string()), - methodNames: optional(array(string())), -}); +export const addKeyPermissionSchema = union([ + object({ + permission: object({ + receiverId: accountId(), + allowance: optional(string()), + methodNames: optional(array(string())), + }), + }), + object({ + permission: literal('FullAccess'), + }), +]); -export const addLimitedKeyAction = object({ +export const addKeyAction = object({ type: literal('AddKey'), params: object({ publicKey: publicKey(), - accessKey: object({ - nonce: optional(serializedBigInt()), - permission: addKeyPermissionSchema, - }), + accessKey: addKeyPermissionSchema, }), }); @@ -108,7 +120,7 @@ export const actionSchema = union([ functionCallAction, transferAction, deleteKeyAction, - addLimitedKeyAction, + addKeyAction, ]); export const transactionSchema = object({ @@ -148,6 +160,11 @@ export const signMessageSchema = object({ network: networkSchema, }); +export const bindNicknameSchema = object({ + nickname: string(), + network: networkSchema, +}); + export const validAccountSchema = object({ network: networkSchema, }); diff --git a/packages/snap/src/core/viewTransactions.ts b/packages/snap/src/core/viewTransactions.ts index c28bdf9..0644c7b 100644 --- a/packages/snap/src/core/viewTransactions.ts +++ b/packages/snap/src/core/viewTransactions.ts @@ -93,7 +93,6 @@ export const viewAction = (receiver: string, action: ActionJson) => { } case 'AddKey': { - // @ts-expect-error FullAccess is prohibited by the superstruct if (action.params.accessKey.permission === 'FullAccess') { view.children.push( heading(action.type), diff --git a/packages/snap/src/data/en.ts b/packages/snap/src/data/en.ts index 57ee81d..2718699 100644 --- a/packages/snap/src/data/en.ts +++ b/packages/snap/src/data/en.ts @@ -10,6 +10,13 @@ export const locales = { silentFunctionCall: 'Call ${STRING} for ${ACCOUNT}', }, + bindNickname: { + site: 'Site **${URL}**', + title: 'Add a nickname to your account', + text: 'HERE has created a nickname for your account for free. Link it so that metamask uses the nickname as an address. **Important! Do not use your hex address if you are attaching a nickname. Any assets must be sent to your nickname address. Do not confirm this action if you did not come up with this nickname.**', + newAddress: 'Your nickname:', + }, + signMessage: { site: 'Site **${URL}**', header: 'Sign Message', diff --git a/packages/snap/src/index.ts b/packages/snap/src/index.ts index d4a266f..6efa8c5 100644 --- a/packages/snap/src/index.ts +++ b/packages/snap/src/index.ts @@ -1,16 +1,24 @@ import { OnRpcRequestHandler } from '@metamask/snaps-types'; -import { connectApp, disconnectApp, getPermissions } from './core/permissions'; import { getAccount, needActivate } from './core/getAccount'; import { signMessage } from './core/signMessage'; import { + bindNickname, + connectApp, + disconnectApp, + getPermissions, +} from './core/permissions'; + +import { + inputAssert, InputAssertError, + bindNicknameSchema, connectWalletSchema, - inputAssert, signDelegateSchema, signMessageSchema, signTransactionsSchema, validAccountSchema, } from './core/validations'; + import { signDelegatedTransaction, signTransactions, @@ -25,6 +33,7 @@ enum Methods { SignTransaction = 'near_signTransactions', SignDelegate = 'near_signDelegate', SignMessage = 'near_signMessage', + BindNickname = 'near_bindNickname', } export const onRpcRequest: OnRpcRequestHandler = async ({ @@ -64,6 +73,12 @@ export const onRpcRequest: OnRpcRequestHandler = async ({ return true; } + case Methods.BindNickname: { + inputAssert(request.params, bindNicknameSchema); + const { network, nickname } = request.params; + return await bindNickname({ network, origin, nickname, snap }); + } + case Methods.SignMessage: { inputAssert(request.params, signMessageSchema); const { network, message, recipient, nonce } = request.params;