Skip to content

Commit

Permalink
v0.7: add bindNickname & fix transactions validator
Browse files Browse the repository at this point in the history
  • Loading branch information
AZbang committed Dec 24, 2023
1 parent ad442b7 commit 5ff7c4e
Show file tree
Hide file tree
Showing 9 changed files with 139 additions and 40 deletions.
4 changes: 2 additions & 2 deletions packages/snap/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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",
Expand Down
4 changes: 2 additions & 2 deletions packages/snap/snap.manifest.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"version": "0.6.1",
"version": "0.7.0",
"description": "Sign transaction for NEAR Protocol. Created by BANYAN and HERE Wallet",
"proposedName": "NEAR Protocol",
"repository": {
"type": "git",
"url": "https://github.com/here-wallet/near-snap.git"
},
"source": {
"shasum": "77c7l1DmnhV9Ja1LdYHefYaYESZoFw7BTMO3PX52VX8=",
"shasum": "UdR3ks5jg2wnyNCGlbnGMaySI22DS5ngPL/JYyUv9hE=",
"location": {
"npm": {
"filePath": "dist/bundle.js",
Expand Down
8 changes: 6 additions & 2 deletions packages/snap/src/core/createAction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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),
);
}

Expand Down
14 changes: 12 additions & 2 deletions packages/snap/src/core/getAccount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -17,6 +17,8 @@ const nearNetwork = {
testnet: 1,
};

export const NICKNAME_KEY = '@nickname';

export async function getKeyPair(
snap: SnapsGlobalObject,
network: NearNetwork,
Expand All @@ -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 };
}

Expand Down
49 changes: 48 additions & 1 deletion packages/snap/src/core/permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -16,6 +16,8 @@ type ConnectOptions = {
contractId?: string;
} & PermissionsPath;

type BindNicknameOptions = { nickname: string } & PermissionsPath;

export async function getPermissions(
data: PermissionsPath,
): Promise<Record<string, string[]> | null> {
Expand All @@ -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',
Expand Down
73 changes: 45 additions & 28 deletions packages/snap/src/core/validations.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { PublicKey } from '@near-js/crypto';
import {
object,
array,
Expand All @@ -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;

Expand All @@ -41,17 +32,34 @@ export const accountId = () =>
);

export const url = () =>
define<string>('url', (value: any) => safeThrowable(() => new URL(value)));
define<string>('url', (value: any) => {
try {
new URL(value);
return true;
} catch {
return false;
}
});

export const publicKey = () =>
define<string>('publicKey', (value: any) =>
safeThrowable(() => PublicKey.fromString(value)),
);
define<string>('publicKey', (value: any) => {
try {
PublicKey.fromString(value);
return true;
} catch (e) {
return false;
}
});

export const serializedBigInt = () =>
define<string>('serializedBigInt', (value: any) =>
safeThrowable(() => BigInt(value)),
);
define<string>('serializedBigInt', (value: any) => {
try {
BigInt(value);
return true;
} catch (e) {
return false;
}
});

export const networkSchemaDefaulted: Describe<NearNetwork> = defaulted(
enums(['testnet', 'mainnet']),
Expand Down Expand Up @@ -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,
}),
});

Expand All @@ -108,7 +120,7 @@ export const actionSchema = union([
functionCallAction,
transferAction,
deleteKeyAction,
addLimitedKeyAction,
addKeyAction,
]);

export const transactionSchema = object({
Expand Down Expand Up @@ -148,6 +160,11 @@ export const signMessageSchema = object({
network: networkSchema,
});

export const bindNicknameSchema = object({
nickname: string(),
network: networkSchema,
});

export const validAccountSchema = object({
network: networkSchema,
});
Expand Down
1 change: 0 additions & 1 deletion packages/snap/src/core/viewTransactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
7 changes: 7 additions & 0 deletions packages/snap/src/data/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
19 changes: 17 additions & 2 deletions packages/snap/src/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -25,6 +33,7 @@ enum Methods {
SignTransaction = 'near_signTransactions',
SignDelegate = 'near_signDelegate',
SignMessage = 'near_signMessage',
BindNickname = 'near_bindNickname',
}

export const onRpcRequest: OnRpcRequestHandler = async ({
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 5ff7c4e

Please sign in to comment.