Skip to content
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

add function access keys to additional contracts by using multiple key stores #887

Closed
10 changes: 10 additions & 0 deletions .gitpod.yml
@@ -0,0 +1,10 @@
# This configuration file was automatically generated by Gitpod.
# Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml)
# and commit this file to your remote git repository to share the goodness with others.

# Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart

tasks:
- init: yarn install


31 changes: 31 additions & 0 deletions examples/react/components/Content.tsx
Expand Up @@ -259,6 +259,31 @@ const Content: React.FC = () => {
[addMessages, getMessages]
);

const handleAddContractConnection = async () => {
const wallet = await selector.wallet();
await wallet.addContractConnection("superduper77.testnet", []);
};

const callWithContractConnection = async () => {
const wallet = await selector.wallet();
const result = await wallet.signAndSendTransaction({
signerId: accountId!,
receiverId: "superduper77.testnet",
actions: [
{
type: "FunctionCall",
params: {
methodName: "call_js_func",
args: { function_name: "nft_metadata" },
gas: BOATLOAD_OF_GAS,
deposit: "0",
},
},
],
});
console.log(JSON.stringify(result));
};

const handleSignMessage = async () => {
const wallet = await selector.wallet();

Expand Down Expand Up @@ -332,6 +357,12 @@ const Content: React.FC = () => {
{accounts.length > 1 && (
<button onClick={handleSwitchAccount}>Switch Account</button>
)}
<button onClick={handleAddContractConnection}>
Add contract connection
</button>
<button onClick={callWithContractConnection}>
callWithContractConnection
</button>
</div>
<Form
account={account}
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/lib/wallet/wallet.types.ts
Expand Up @@ -239,6 +239,10 @@ export type BrowserWalletBehaviour = Modify<
buildImportAccountsUrl?(): string;
importAccountsInSecureContext?: never;
signIn(params: BrowserWalletSignInParams): Promise<Array<Account>>;
addContractConnection?(
contractId: string,
methodNames: Array<string>
): Promise<void>;
signAndSendTransaction(
params: BrowserWalletSignAndSendTransactionParams
): Promise<FinalExecutionOutcome | void>;
Expand Down
121 changes: 68 additions & 53 deletions packages/my-near-wallet/src/lib/my-near-wallet.spec.ts
@@ -1,15 +1,18 @@
/* eslint-disable @nx/enforce-module-boundaries */
import { ConnectedWalletAccount } from "near-api-js";

Check failure on line 2 in packages/my-near-wallet/src/lib/my-near-wallet.spec.ts

View workflow job for this annotation

GitHub Actions / Test Suite

All imports in the declaration are only used as types. Use `import type`
import type { Near, WalletConnection } from "near-api-js";
import type {

Check failure on line 4 in packages/my-near-wallet/src/lib/my-near-wallet.spec.ts

View workflow job for this annotation

GitHub Actions / Test Suite

Replace `⏎··FinalExecutionOutcome,⏎` with `·FinalExecutionOutcome·`
Near,
WalletConnection,
ConnectedWalletAccount,
} from "near-api-js";
import type { AccountView } from "near-api-js/lib/providers/provider";
FinalExecutionOutcome,
} from "near-api-js/lib/providers/provider";
import { type AccountView } from "near-api-js/lib/providers/provider";
import { mock } from "jest-mock-extended";

import { mockWallet } from "../../../core/src/lib/testUtils";
import type { MockWalletDependencies } from "../../../core/src/lib/testUtils";
import type { BrowserWallet } from "../../../core/src/lib/wallet";
import type {

Check failure on line 12 in packages/my-near-wallet/src/lib/my-near-wallet.spec.ts

View workflow job for this annotation

GitHub Actions / Test Suite

Replace `⏎··BrowserWallet,⏎` with `·BrowserWallet·`
BrowserWallet,
} from "../../../core/src/lib/wallet";
import { SignAndSendTransactionOptions } from "near-api-js/lib/account";

Check failure on line 15 in packages/my-near-wallet/src/lib/my-near-wallet.spec.ts

View workflow job for this annotation

GitHub Actions / Test Suite

All imports in the declaration are only used as types. Use `import type`

const createMyNearWallet = async (deps: MockWalletDependencies = {}) => {
const walletConnection = mock<WalletConnection>();
Expand Down Expand Up @@ -57,8 +60,19 @@
};
};

beforeEach(() => {
const borsh = require('borsh');

Check warning on line 64 in packages/my-near-wallet/src/lib/my-near-wallet.spec.ts

View workflow job for this annotation

GitHub Actions / Test Suite

Require statement not part of import statement

Check failure on line 64 in packages/my-near-wallet/src/lib/my-near-wallet.spec.ts

View workflow job for this annotation

GitHub Actions / Test Suite

Replace `'borsh'` with `"borsh"`

const originalBaseDecode = borsh.baseDecode;
borsh.baseDecode = function (value: string) {
const bufferResult = originalBaseDecode(value);
return new Uint8Array(bufferResult);
};
});

afterEach(() => {
jest.resetModules();
jest.unmock("near-api-js");
});

describe("signIn", () => {
Expand All @@ -71,64 +85,65 @@
});
});

describe("signOut", () => {
it("sign out of near wallet", async () => {
const { wallet, walletConnection } = await createMyNearWallet();

await wallet.signIn({ contractId: "test.testnet" });
await wallet.signOut();

expect(walletConnection.signOut).toHaveBeenCalled();
});
});

describe("getAccounts", () => {
it("returns array of accounts", async () => {
const { wallet, walletConnection } = await createMyNearWallet();
describe("multipleAppSignin", () => {
it("should choose the appropriate function access key for transaction", async () => {
const nearAPI = require("near-api-js");

Check warning on line 90 in packages/my-near-wallet/src/lib/my-near-wallet.spec.ts

View workflow job for this annotation

GitHub Actions / Test Suite

Require statement not part of import statement
const mainKeyPair = nearAPI.KeyPair.fromRandom("ed25519");
const accountId = "testaccount.testnet";
localStorage.setItem(
"near_app_wallet_auth_key",
`{"accountId":"${accountId}","allKeys":["ed25519:EjYAuzTBj3ZHeznV71HuWC5f3Yqrty7AVYWhPKufcqGf"]}`
);

await wallet.signIn({ contractId: "test.testnet" });
const result = await wallet.getAccounts();
nearAPI.ConnectedWalletAccount.prototype.signAndSendTransaction = async function (

Check failure on line 98 in packages/my-near-wallet/src/lib/my-near-wallet.spec.ts

View workflow job for this annotation

GitHub Actions / Test Suite

Replace `·async·function·(⏎······options:·SignAndSendTransactionOptions⏎····` with `⏎······async·function·(options:·SignAndSendTransactionOptions`
options: SignAndSendTransactionOptions

Check failure on line 99 in packages/my-near-wallet/src/lib/my-near-wallet.spec.ts

View workflow job for this annotation

GitHub Actions / Test Suite

'options' is defined but never used
) {
sessionStorage.setItem(

Check failure on line 101 in packages/my-near-wallet/src/lib/my-near-wallet.spec.ts

View workflow job for this annotation

GitHub Actions / Test Suite

Insert `··`
"lastAccessedKey",

Check failure on line 102 in packages/my-near-wallet/src/lib/my-near-wallet.spec.ts

View workflow job for this annotation

GitHub Actions / Test Suite

Insert `··`
(await this.walletConnection._keyStore.getKey(networkId, accountId))

Check failure on line 103 in packages/my-near-wallet/src/lib/my-near-wallet.spec.ts

View workflow job for this annotation

GitHub Actions / Test Suite

Insert `··`
.getPublicKey()
.toString()
);
return mock<FinalExecutionOutcome>();
};

expect(walletConnection.getAccountId).toHaveBeenCalled();
expect(result).toEqual([
{ accountId: "test-account.testnet", publicKey: "" },
]);
});
});
const networkId = "testnet";
const mainKeyStore = new nearAPI.keyStores.BrowserLocalStorageKeyStore();
mainKeyStore.setKey(networkId, accountId, mainKeyPair);

describe("signAndSendTransaction", () => {
// TODO: Figure out why imports to core are returning undefined.
it("signs and sends transaction", async () => {
const { wallet, walletConnection, account } = await createMyNearWallet();
const { setupMyNearWallet } = require("./my-near-wallet");

Check warning on line 114 in packages/my-near-wallet/src/lib/my-near-wallet.spec.ts

View workflow job for this annotation

GitHub Actions / Test Suite

Require statement not part of import statement
const { wallet } = await mockWallet<BrowserWallet>(setupMyNearWallet(), {});

await wallet.signIn({ contractId: "test.testnet" });
const result = await wallet.signAndSendTransaction({
receiverId: "guest-book.testnet",
await wallet.signAndSendTransaction({
receiverId: "test.testnet",
actions: [],
});

expect(walletConnection.account).toHaveBeenCalled();
// near-api-js marks this method as protected.
// @ts-ignore
expect(account.signAndSendTransaction).toHaveBeenCalled();
// @ts-ignore
expect(account.signAndSendTransaction).toBeCalledWith({
actions: [],
receiverId: "guest-book.testnet",
});
expect(result).toEqual(null);
});
});
expect(sessionStorage.getItem("lastAccessedKey")).toEqual(
mainKeyPair.getPublicKey().toString()
);
const additionalContractId = "test2.testnet";
await wallet.addContractConnection!(additionalContractId, []);

describe("buildImportAccountsUrl", () => {
it("returns import url", async () => {
const { wallet } = await createMyNearWallet();
const keyStore = new nearAPI.keyStores.BrowserLocalStorageKeyStore(
window.localStorage,
`${additionalContractId}:keystore:`
);

expect(typeof wallet.buildImportAccountsUrl).toBe("function");
const account = (await wallet.getAccounts())[0];
const additionalKeypair = await keyStore.getKey(
"testnet",
account.accountId
);
expect(additionalKeypair).toBeDefined();

// @ts-ignore
expect(wallet?.buildImportAccountsUrl()).toEqual(
"https://testnet.mynearwallet.com/batch-import"
await wallet.signAndSendTransaction({
receiverId: "test2.testnet",
actions: [],
});
expect(sessionStorage.getItem("lastAccessedKey")).toEqual(
additionalKeypair.getPublicKey().toString()
);
});
});
106 changes: 98 additions & 8 deletions packages/my-near-wallet/src/lib/my-near-wallet.ts
Expand Up @@ -68,7 +68,9 @@ const MyNearWallet: WalletBehaviourFactory<
BrowserWallet,
{ params: MyNearWalletExtraOptions }
> = async ({ metadata, options, store, params, logger }) => {
const _state = await setupWalletState(params, options.network);
const _state = {
...(await setupWalletState(params, options.network)),
};
const getAccounts = async (): Promise<Array<Account>> => {
const accountId = _state.wallet.getAccountId();
const account = _state.wallet.account();
Expand All @@ -88,6 +90,32 @@ const MyNearWallet: WalletBehaviourFactory<
},
];
};
const getAccountObjectForTargetContract = async (targetContract: string, callerAccountId: string, networkId: string): Promise<nearAPI.ConnectedWalletAccount | null> => {
const keyStore = new nearAPI.keyStores.BrowserLocalStorageKeyStore(
window.localStorage,
`${targetContract}:keystore:`
);

if (await keyStore.getKey(_state.wallet._networkId, callerAccountId)) {
console.log('using key for target contract', targetContract);
const appPrefix = targetContract;
localStorage.setItem(
`${appPrefix}_wallet_auth_key`,
localStorage.getItem("near_app_wallet_auth_key")!
);
const near = await nearAPI.connect({
keyStore,
walletUrl: params.walletUrl,
networkId: networkId,
nodeUrl: `https://rpc.${networkId}.near.org`,
headers: {},
});
const walletConnection = new nearAPI.WalletConnection(near, appPrefix);
return walletConnection.account();
} else {
return null;
}
};

const transformTransactions = async (
transactions: Array<Optional<Transaction, "signerId">>
Expand Down Expand Up @@ -146,6 +174,49 @@ const MyNearWallet: WalletBehaviourFactory<
return getAccounts();
},

async addContractConnection(
contractId: string,
methodNames: Array<string>
) {
const account = _state.wallet.account();
const keyStore = new nearAPI.keyStores.BrowserLocalStorageKeyStore(
window.localStorage,
`${contractId}:keystore:`
);

if (await keyStore.getKey(_state.wallet._networkId,account.accountId)) {
return;
}

// Create a new random key pair for the access key

const keyPair = nearAPI.utils.KeyPair.fromRandom("ed25519");

const permission = nearAPI.transactions.functionCallAccessKey(
contractId,
methodNames
);

// Construct the transaction
const actions = [
nearAPI.transactions.addKey(keyPair.getPublicKey(), permission),
];



await keyStore.setKey(
_state.wallet._networkId,
account.accountId,
keyPair
);

// Sign and send the transaction
await account.signAndSendTransaction({
receiverId: account.accountId,
actions,
});
},

async signOut() {
if (_state.wallet.isSignedIn()) {
_state.wallet.signOut();
Expand Down Expand Up @@ -174,18 +245,23 @@ const MyNearWallet: WalletBehaviourFactory<
});

const { contract } = store.getState();
let account = _state.wallet.account();

if (!_state.wallet.isSignedIn() || !contract) {
throw new Error("Wallet not signed in");
}

const account = _state.wallet.account();
const targetContract = receiverId || contract.contractId;

account = await getAccountObjectForTargetContract(targetContract, account.accountId, account.connection.networkId) || account;

return account["signAndSendTransaction"]({
receiverId: receiverId || contract.contractId,
const result = account.signAndSendTransaction({
receiverId: targetContract,
actions: actions.map((action) => createAction(action)),
walletCallbackUrl: callbackUrl,
});

return result;
},

async signAndSendTransactions({ transactions, callbackUrl }) {
Expand All @@ -195,10 +271,24 @@ const MyNearWallet: WalletBehaviourFactory<
throw new Error("Wallet not signed in");
}

return _state.wallet.requestSignTransactions({
transactions: await transformTransactions(transactions),
callbackUrl,
});
if (transactions.length == 1) {
const transaction = transactions[0];

const receiverId = transaction.receiverId;
let account = _state.wallet.account();
account = await getAccountObjectForTargetContract(receiverId, account.accountId, account.connection.networkId) || account;

await account.signAndSendTransaction({
receiverId,
actions: transaction.actions.map((action) => createAction(action)),
walletCallbackUrl: callbackUrl
});
} else {
return _state.wallet.requestSignTransactions({
transactions: await transformTransactions(transactions),
callbackUrl,
});
}
},

buildImportAccountsUrl() {
Expand Down