Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 105 additions & 92 deletions client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { createKey} from "@near-js/biometric-ed25519";
import { Action, encodeSignedDelegate } from "@near-js/transactions";
import { Account, KeyPair} from "near-api-js";
import { encryptPrivateKey, storeEncryptedAccount } from "./utils";
import { createAccountRequest, getBiometricAccount, getPasswordBasedAccount } from "./account";
import { createKey } from '@near-js/biometric-ed25519';
import { Action, encodeSignedDelegate } from '@near-js/transactions';
import { Account, KeyPair } from 'near-api-js';
import { encryptPrivateKey, storeEncryptedAccount } from './utils';
import {
createAccountRequest,
getBiometricAccount,
getPasswordBasedAccount,
} from './account';
/**
* Generates a new keypair locally and stores it in passkey, local storage, or uses a provided keypair,
* then sends the account ID and publicKey to a relayer to be created on chain via a smart contract call
*
*
* @param {string} relayerUrl - The URL of the server to send the request to.
* @param {string} accountId - The ID of the account to create.
* @param {Object} options - Optional parameters for account creation
Expand All @@ -15,43 +19,53 @@ import { createAccountRequest, getBiometricAccount, getPasswordBasedAccount } fr
* @param {boolean} options.usePasskey - Whether to use passkey for key generation
* @returns {Promise<any>} - A promise that resolves to the response from the server.
*/
export async function createAccount(relayerUrl: string, accountId: string, options: {
keyPair?: KeyPair,
password?: string,
usePasskey?: boolean
} = {}): Promise<any> {
let key: KeyPair;
export async function createAccount(
relayerUrl: string,
accountId: string,
options: {
keyPair?: KeyPair;
password?: string;
usePasskey?: boolean;
} = {},
): Promise<any> {
let key: KeyPair;

if (options.keyPair) {
key = options.keyPair;
} else if (options.usePasskey) {
key = await createKey(accountId);
} else {
key = KeyPair.fromRandom('ed25519');
}

const publicKey = key.getPublicKey().toString();

try {
const response = await createAccountRequest(
relayerUrl,
accountId,
publicKey,
);

if (options.keyPair) {
key = options.keyPair;
} else if (options.usePasskey) {
key = await createKey(accountId);
} else {
key = KeyPair.fromRandom('ed25519');
if (options.password) {
const privateKey = (key as any).secretKey;
const encryptedPrivateKey = encryptPrivateKey(
privateKey,
options.password,
);
storeEncryptedAccount(encryptedPrivateKey, accountId, publicKey);
}

const publicKey = key.getPublicKey().toString();

try {
const response = await createAccountRequest(relayerUrl, accountId, publicKey);

if (options.password) {
const privateKey = (key as any).secretKey;
const encryptedPrivateKey = encryptPrivateKey(privateKey, options.password);
storeEncryptedAccount(encryptedPrivateKey, accountId, publicKey);
}

return response;
} catch (error) {
console.error('Failed to create account:', error);
throw error;
}
return response;
} catch (error) {
console.error('Failed to create account:', error);
throw error;
}
}


/**
* Signs a transaction and sends it to a relayer to get dispatched
*
* Signs a transaction and sends it to a relayer to get dispatched
*
* @param {Action[]} action - The list of actions to include in the transaction.
* @param {string} receiverId - The ID of the receiver.
* @param {string} relayerUrl - Url for the relayer to which the tx will be sent
Expand All @@ -63,65 +77,64 @@ export async function createAccount(relayerUrl: string, accountId: string, optio
* @throws {Error} - If there is an error relaying the transaction.
*/
export async function relayTransaction(
action: Action | Action[],
receiverId: string,
relayerUrl: string,
network: string = 'mainnet',
options: {
account?: Account,
password?: string
} = {}
action: Action | Action[],
receiverId: string,
relayerUrl: string,
network: string = 'mainnet',
options: {
account?: Account;
password?: string;
} = {},
) {

let account = options.account;

if (options.password) {
account = await getPasswordBasedAccount(options.password, network);
} else {
account = await getBiometricAccount(network);
}

if (!account) {
console.error("Failed to retrieve or create an account");
throw new Error("Failed to retrieve or create an account.");
}

const signedDelegate = await account.signedDelegate({
actions: Array.isArray(action) ? action : [action],
blockHeightTtl: 60,
receiverId: receiverId,
});

const result = await relayRequest(signedDelegate, relayerUrl);
return result;
let account = options.account;

if (options.password) {
account = await getPasswordBasedAccount(options.password, network);
} else {
account = await getBiometricAccount(network);
}

if (!account) {
console.error('Failed to retrieve or create an account');
throw new Error('Failed to retrieve or create an account.');
}

const signedDelegate = await account.signedDelegate({
actions: Array.isArray(action) ? action : [action],
blockHeightTtl: 60,
receiverId: receiverId,
});

const result = await relayRequest([signedDelegate], relayerUrl);
return result;
}

async function relayRequest(
signedDelegate: any,
relayerUrl: string,
): Promise<any> {
const headers = new Headers({ 'Content-Type': 'application/json' });
const bitteApiKey = process.env.BITTE_API_KEY;
if (bitteApiKey) {
headers.append('bitte-api-key', bitteApiKey);
}

try {
const encodedDelegate = encodeSignedDelegate(signedDelegate);
const res = await fetch(relayerUrl, {
method: 'POST',
mode: 'cors',
body: JSON.stringify(Array.from(encodedDelegate)),
headers: headers,
});

async function relayRequest(signedDelegate: any, relayerUrl: string): Promise<any> {
const headers = new Headers({ "Content-Type": "application/json" });
const bitteApiKey = process.env.BITTE_API_KEY;
if (bitteApiKey) {
headers.append("bitte-api-key", bitteApiKey);
if (!res.ok) {
const errorText = await res.text();
throw new Error(`Error: ${res.status} ${res.statusText} - ${errorText}`);
}

try {
const encodedDelegate = encodeSignedDelegate(signedDelegate);
const res = await fetch(relayerUrl, {
method: "POST",
mode: "cors",
body: JSON.stringify(Array.from(encodedDelegate)),
headers: headers,
});

if (!res.ok) {
const errorText = await res.text();
throw new Error(`Error: ${res.status} ${res.statusText} - ${errorText}`);
}

return await res.json();
} catch (e: any) {
throw new Error(`Failed to relay transaction: ${e.message}`);
}
return await res.json();
} catch (e: any) {
throw new Error(`Failed to relay transaction: ${e.message}`);
}
}


80 changes: 48 additions & 32 deletions server/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@

import 'dotenv/config'
import 'dotenv/config';
import { deserialize } from 'borsh';
import { Account, connect, KeyPair } from "near-api-js";
import { SCHEMA, SignedDelegate, actionCreators } from "@near-js/transactions";
import { Account, connect, KeyPair } from 'near-api-js';
import { SCHEMA, SignedDelegate, actionCreators } from '@near-js/transactions';
import { FinalExecutionOutcome } from 'near-api-js/lib/providers';
import BN from 'bn.js';
import { InMemoryKeyStore } from 'near-api-js/lib/key_stores';
Expand All @@ -28,28 +27,41 @@ export interface RelayOptions {
* @returns A promise that resolves to the final execution outcome.
* @throws An error if required options are missing.
*/
export async function relay(encodedDelegate: Buffer, options: RelayOptions = {}): Promise<FinalExecutionOutcome> {

export async function relay(
encodedDelegates: Buffer[],
): Promise<FinalExecutionOutcome[]> {
if (!network) {
throw new Error("Network must be provided as an environment variable or argument.");
throw new Error(
'Network must be provided as an environment variable or argument.',
);
}
if (!relayerAccountId) {
throw new Error("Relayer account ID must be provided as an environment variable or argument.");
throw new Error(
'Relayer account ID must be provided as an environment variable or argument.',
);
}
if (!relayerPrivateKey) {
throw new Error("Relayer private key must be provided as an environment variable or argument.");
throw new Error(
'Relayer private key must be provided as an environment variable or argument.',
);
}

const deserializedTx: SignedDelegate = deserialize(SCHEMA.SignedDelegate, Buffer.from(encodedDelegate)) as SignedDelegate;

const relayerAccount: Account = await getRelayer();

const receipt = await relayerAccount.signAndSendTransaction({
actions: [actionCreators.signedDelegate(deserializedTx)],
receiverId: deserializedTx.delegateAction.senderId
});
const outcomes = await Promise.all(
encodedDelegates.map((encodedDelegate) => {
const deserializedTx: SignedDelegate = deserialize(
SCHEMA.SignedDelegate,
Buffer.from(encodedDelegate),
) as SignedDelegate;

return receipt;
return relayerAccount.signAndSendTransaction({
actions: [actionCreators.signedDelegate(deserializedTx)],
receiverId: deserializedTx.delegateAction.senderId,
});
}),
);

return outcomes;
}

/**
Expand All @@ -63,18 +75,18 @@ export async function createAccount(
accountId: string,
publicKey: string,
): Promise<FinalExecutionOutcome> {
try {
try {
const relayerAccount = await getRelayer();

const result = await relayerAccount.functionCall({
contractId: network === "mainnet" ? "near" : "testnet",
methodName: "create_account",
contractId: network === 'mainnet' ? 'near' : 'testnet',
methodName: 'create_account',
args: {
new_account_id: accountId,
new_public_key: publicKey,
},
gas: new BN("300000000000000"),
attachedDeposit: new BN("0"),
gas: new BN('300000000000000'),
attachedDeposit: new BN('0'),
});

return result;
Expand All @@ -89,15 +101,19 @@ export async function createAccount(
* @return {Promise<Account>} The relayer account object.
*/
async function getRelayer(): Promise<Account> {
const keyStore: InMemoryKeyStore = new InMemoryKeyStore();
await keyStore.setKey(network, relayerAccountId, KeyPair.fromString(relayerPrivateKey));
const keyStore: InMemoryKeyStore = new InMemoryKeyStore();
await keyStore.setKey(
network,
relayerAccountId,
KeyPair.fromString(relayerPrivateKey),
);

const config = {
networkId: network,
keyStore,
nodeUrl: `https://rpc.${network}.near.org`,
};
const config = {
networkId: network,
keyStore,
nodeUrl: `https://rpc.${network}.near.org`,
};

const near = await connect(config);
return await near.account(relayerAccountId);
}
const near = await connect(config);
return await near.account(relayerAccountId);
}