Skip to content

Commit

Permalink
added wallet connections (ton-blockchain#3)
Browse files Browse the repository at this point in the history
* added wallet connections

* a

* a

* Wallet connections fixes (ton-blockchain#7)

* prettier + fix tests

* -

Co-authored-by: Shahar Yakir <shahar.yakir@gmail.com>
  • Loading branch information
denis-orbs and shaharyakir authored Jun 8, 2022
1 parent fae2730 commit 2f96d24
Show file tree
Hide file tree
Showing 18 changed files with 727 additions and 45 deletions.
2 changes: 1 addition & 1 deletion index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@ export * from "./lib/deploy-controller";
export * from "./lib/env-profiles";
export * from "./lib/contract-deployer";
export * from "./lib/transaction-sender";
export * from "./lib/file-uploader";
export * from "./lib/wallets";
9 changes: 6 additions & 3 deletions lib/contract-deployer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TransactionSender } from "./transaction-sender";
import BN from "bn.js";
import { Address, Cell, contractAddress, StateInit } from "ton";
import { WalletService } from "./wallets";
import { Adapters } from "./wallets/types";

interface ContractDeployDetails {
deployer: Address;
Expand All @@ -22,12 +23,14 @@ export class ContractDeployer {

async deployContract(
params: ContractDeployDetails,
transactionSender: TransactionSender
adapterId: Adapters,
session: any,
walletService: WalletService
): Promise<Address> {
const _contractAddress = this.addressForContract(params);

if (!params.dryRun) {
await transactionSender.sendTransaction({
await walletService.requestTransaction(adapterId, session, {
to: _contractAddress,
value: params.value,
stateInit: new StateInit({ data: params.data, code: params.code }),
Expand Down
8 changes: 4 additions & 4 deletions lib/deploy-controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import BN from "bn.js";
import { Address, beginCell, Cell, toNano, TonClient } from "ton";
import { TransactionSender } from "./transaction-sender";
import { ContractDeployer } from "./contract-deployer";

// TODO temporary
Expand All @@ -13,6 +12,7 @@ import {
JETTON_MINTER_CODE,
parseOnChainData,
} from "../contracts/jetton-minter";
import { Adapters } from "./wallets/types";
axiosThrottle.use(axios, { requestsPerSecond: 0.9 }); // required since toncenter jsonRPC limits to 1 req/sec without API key

export const JETTON_DEPLOY_GAS = toNano(0.25);
Expand Down Expand Up @@ -47,11 +47,11 @@ export class JettonDeployController {
this.#client = client;
}

// TODO change jettonIconImageData to jettonIconURI
async createJetton(
params: JettonDeployParams,
contractDeployer: ContractDeployer,
transactionSender: TransactionSender
adapterId: Adapters,
session: any
) {
params.onProgress?.(JettonDeployState.BALANCE_CHECK);
const balance = await this.#client.getBalance(params.owner);
Expand All @@ -77,7 +77,7 @@ export class JettonDeployController {
if (await this.#client.isContractDeployed(contractAddr)) {
params.onProgress?.(JettonDeployState.ALREADY_DEPLOYED);
} else {
await contractDeployer.deployContract(deployParams, transactionSender);
await contractDeployer.deployContract(deployParams, adapterId, session);
params.onProgress?.(JettonDeployState.AWAITING_MINTER_DEPLOY);
await waitForContractDeploy(contractAddr, this.#client);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export async function waitForSeqno(wallet: Wallet) {

export async function waitForContractDeploy(address: Address, client: TonClient) {
let isDeployed = false;
let maxTries = 10;
let maxTries = 25;
while (!isDeployed && maxTries > 0) {
maxTries--;
isDeployed = await client.isContractDeployed(address);
Expand Down
51 changes: 51 additions & 0 deletions lib/wallets/WalletService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Cell } from "ton";
import { TonhubConnector } from "ton-x";
import { TransactionDetails } from "../transaction-sender";
import { TonChromeExtensionWalletAdapter, TonhubWalletAdapter } from "./adapters";

import { WalletAdapter, Wallet, Adapters } from "./types";

const IS_TESTNET = false;

export class WalletService {
private readonly adapters: Map<string, WalletAdapter<any>> = new Map();

registerAdapter(adapterId: string, adapter: WalletAdapter<any>) {
console.log(this.adapters);

this.adapters.set(adapterId, adapter);
}

constructor() {
this.registerAdapter(Adapters.TON_WALLET, new TonChromeExtensionWalletAdapter());
this.registerAdapter(Adapters.TON_HUB, new TonhubWalletAdapter(IS_TESTNET));
}

createSession<S>(adapterId: string, appName: string): Promise<S> {
const adapter = this.adapters.get(adapterId) as WalletAdapter<S>;
return adapter.createSession(appName);
}

async awaitReadiness<S>(adapterId: string, session: S): Promise<Wallet> {
const adapter = this.adapters.get(adapterId) as WalletAdapter<S>;
return adapter.awaitReadiness(session);
}

async getWallet<S>(adapterId: string, session: S): Promise<Wallet> {
const adapter = this.adapters.get(adapterId) as WalletAdapter<S>;
return adapter.getWallet(session);
}

async requestTransaction<S>(
adapterId: string,
session: any,
request: TransactionDetails,
onSuccess?: () => void
): Promise<void | boolean> {
const adapter = this.adapters.get(adapterId) as WalletAdapter<S>;

return adapter.requestTransaction(session, request, onSuccess);
}
}

export default WalletService;
68 changes: 68 additions & 0 deletions lib/wallets/adapters/TonChromeExtensionWalletAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { Cell } from "ton";
import { TransactionDetails } from "../../transaction-sender";
import { tonWalletClient } from "../clients/TonWalletClient";
import { TON_WALLET_EXTENSION_URL } from "../config";
import { TransactionRequest, Wallet, WalletAdapter } from "../types";

export function delay(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export class TonChromeExtensionWalletAdapter implements WalletAdapter<boolean> {
async createSession(): Promise<boolean> {
try {
await tonWalletClient.ready(150);
return true;
} catch (error) {
window.open(TON_WALLET_EXTENSION_URL, "_blank");
throw error;
}
}

async awaitReadiness(session: boolean): Promise<Wallet> {
await tonWalletClient.ready();

const [[wallet]] = await Promise.all([tonWalletClient.requestWallets(), delay(150)]);

if (!wallet) {
throw new Error("TON Wallet is not configured.");
}

return wallet;
}

getWallet(session: boolean): Promise<Wallet> {
return this.awaitReadiness(session);
}

isAvailable(): boolean {
return !!(window as any).ton?.isTonWallet;
}

async requestTransaction(
_session: any,
request: TransactionDetails,
onSuccess?: () => void
): Promise<void> {
const INIT_CELL = new Cell();
request.stateInit.writeTo(INIT_CELL);
const b64InitCell = INIT_CELL.toBoc().toString("base64");

try {
const res: any = await tonWalletClient.sendTransaction({
to: request.to.toFriendly(),
value: request.value.toString(),
dataType: "boc",
data: request.message?.toBoc().toString("base64"),
stateInit: b64InitCell,
});

if (!res) {
throw new Error("Something went wrong");
} else {
onSuccess && onSuccess();
}
} catch (error: any) {
throw new Error(error.message);
}
}
}
96 changes: 96 additions & 0 deletions lib/wallets/adapters/TonhubWalletAdapter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Cell, ConfigStore } from "ton";
import { TonhubConnector } from "ton-x";
import { TonhubCreatedSession } from "ton-x/dist/connector/TonhubConnector";
import { TransactionDetails } from "../../transaction-sender";
import { TransactionRequest, Wallet, WalletAdapter } from "../types";

const TONHUB_TIMEOUT = 5 * 60 * 1000;

export class TonhubWalletAdapter implements WalletAdapter<TonhubCreatedSession> {
tonhubConnector = new TonhubConnector();
constructor(testnet: boolean) {
this.tonhubConnector = new TonhubConnector({ testnet });
}

createSession(name: string): Promise<TonhubCreatedSession> {
const { location } = document;

return this.tonhubConnector.createNewSession({
name: name,
url: `${location.protocol}//${location.host}`,
});
}

async awaitReadiness(session: TonhubCreatedSession): Promise<Wallet> {
const state = await this.tonhubConnector.awaitSessionReady(session.id, TONHUB_TIMEOUT, 0);

if (state.state === "revoked") {
throw new Error("Connection was cancelled.");
}

if (state.state === "expired") {
throw new Error("Connection was not confirmed.");
}

const walletConfig = new ConfigStore(state.wallet.walletConfig);

return {
address: state.wallet.address,
publicKey: walletConfig.getString("pk"),
walletVersion: state.wallet.walletType,
};
}

getWallet(session: TonhubCreatedSession): Promise<Wallet> {
return this.awaitReadiness(session);
}

async requestTransaction(
session: TonhubCreatedSession,
request: TransactionDetails,
onSuccess?: () => void
): Promise<void> {
const state = await this.tonhubConnector.getSessionState(session.id);

if (state.state !== "ready") {
throw new Error("State is not ready");
}

const INIT_CELL = new Cell();
request.stateInit.writeTo(INIT_CELL);
const b64InitCell = INIT_CELL.toBoc().toString("base64");

const response = await this.tonhubConnector.requestTransaction({
seed: session.seed,
appPublicKey: state.wallet.appPublicKey,
to: request.to.toFriendly(),
value: request.value.toString(),
timeout: 5 * 60 * 1000,
stateInit: b64InitCell,
// text: request.text,
payload: request.message?.toBoc().toString("base64"),
});

if (response.type === "rejected") {
throw new Error("Transaction was rejected.");
}

if (response.type === "expired") {
throw new Error("Transaction was expired.");
}

if (response.type === "invalid_session") {
throw new Error("Something went wrong. Refresh the page and try again.");
}

if (response.type === "success") {
onSuccess && onSuccess();
// Handle successful transaction
// const externalMessage = response.response; // Signed external message that was sent to the network
}
}

isAvailable(): boolean {
return true;
}
}
2 changes: 2 additions & 0 deletions lib/wallets/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./TonChromeExtensionWalletAdapter";
export * from "./TonhubWalletAdapter";
58 changes: 58 additions & 0 deletions lib/wallets/clients/TonWalletClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { TonWalletProvider, Wallet } from "../types";

declare global {
interface Window {
ton?: TonWalletProvider;
}
}

export class TonWalletClient {
constructor(private readonly window: Window) {}

private get ton(): TonWalletProvider | undefined {
return this.window.ton;
}

get isAvailable(): boolean {
return !!this.ton?.isTonWallet;
}

ready(timeout = 5000): Promise<void> {
return new Promise((resolve, reject) => {
const timerId = setInterval(() => {
if (this.isAvailable) {
clearInterval(timerId);
resolve();
}
}, 50);

setTimeout(() => reject(new Error("TON Wallet cannot be initialized")), timeout);
});
}

requestWallets(): Promise<Wallet[]> {
return this.ton!.send("ton_requestWallets");
}

watchAccounts(callback: (accounts: string[]) => void): void {
this.ton!.on("ton_requestAccounts", callback);
}

sign(hexData: string): Promise<string> {
return this.ton!.send("ton_rawSign", [{ data: hexData }]);
}

sendTransaction(options: {
to: string;
value: string;
data?: string;
dataType?: "boc" | "hex" | "base64" | "text";
stateInit?: string;
}): Promise<void> {
return this.ton!.send("ton_sendTransaction", [options]);
}
}

global["window"] = global["window"] ?? null;

export const tonWalletClient = new TonWalletClient(window);
1 change: 1 addition & 0 deletions lib/wallets/clients/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./TonWalletClient";
19 changes: 19 additions & 0 deletions lib/wallets/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Adapter, Adapters } from "./types";

const adapters: Adapter[] = [
{
text: "Tonhub",
type: Adapters.TON_HUB,
mobileCompatible: true,
},
{
text: "Ton Wallet",
type: Adapters.TON_WALLET,
mobileCompatible: false,
},
];

const TON_WALLET_EXTENSION_URL =
"https://chrome.google.com/webstore/detail/ton-wallet/nphplpgoakhhjchkkhmiggakijnkhfnd";

export { adapters, TON_WALLET_EXTENSION_URL };
5 changes: 5 additions & 0 deletions lib/wallets/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export * from "./WalletService";
export * from "./clients";
export * from "./config";
export * from "./adapters";
export * from "./methods";
Loading

0 comments on commit 2f96d24

Please sign in to comment.