From b31fa5f931c5bef624e7d8631f8d6b2209f62489 Mon Sep 17 00:00:00 2001 From: mrmld Date: Tue, 7 Jun 2022 19:18:55 +0400 Subject: [PATCH 1/4] draft: ton-x transport & separate from tonhub http api --- src/connector/TonhubConnector.ts | 61 ++++++++++++------- src/transport/TonXTransport.ts | 25 ++++++++ src/transport/TonhubHttpTransport.ts | 87 ++++++++++++++++++++++++++++ src/transport/Transport.ts | 3 + 4 files changed, 156 insertions(+), 20 deletions(-) create mode 100644 src/transport/TonXTransport.ts create mode 100644 src/transport/TonhubHttpTransport.ts create mode 100644 src/transport/Transport.ts diff --git a/src/connector/TonhubConnector.ts b/src/connector/TonhubConnector.ts index 4de4470..3d0a363 100644 --- a/src/connector/TonhubConnector.ts +++ b/src/connector/TonhubConnector.ts @@ -7,6 +7,8 @@ import { delay } from "teslabot"; import { Cell, Address, beginCell, CommentMessage, safeSign, contractAddress, safeSignVerify } from 'ton'; import BN from 'bn.js'; import { WalletV4Source } from 'ton-contracts'; +import { TonhubHttpTransport } from '../transport/TonhubHttpTransport'; +import { TonXTransport } from '../transport/TonXTransport'; const sessionStateCodec = t.union([ t.type({ @@ -166,6 +168,16 @@ function textToCell(src: string) { return res; } +function autodiscoverTransport(config?: { adapter: any }) { + if (TonXTransport.isAvailable()) { + return new TonXTransport(); + } + return new TonhubHttpTransport({ + adapter: config?.adapter, + endpoint: 'https://connect.tonhubapi.com' + }); +} + export class TonhubConnector { static extractPublicKey(config: { @@ -265,9 +277,10 @@ export class TonhubConnector { } readonly testnet: boolean; - readonly adapter: any | null; + readonly transport: Transport; + - constructor(args?: { testnet?: boolean, adapter?: any }) { + constructor(args?: { testnet?: boolean, adapter?: any, transport?: Transport }) { let testnet = false; let adapter: any | null = null; if (args) { @@ -279,7 +292,7 @@ export class TonhubConnector { } } this.testnet = testnet; - this.adapter = adapter; + this.transport = args?.transport || autodiscoverTransport({ adapter }); } createNewSession = async (args: { name: string, url: string }): Promise => { @@ -291,12 +304,14 @@ export class TonhubConnector { // Request new session await backoff(async () => { - let session = await axios.post('https://connect.tonhubapi.com/connect/init', { + let session = await this.transport.call('session_new', { key: sessionId, testnet: this.testnet, name: args.name, - url: args.url - }, this.adapter ? { timeout: 5000, adapter: this.adapter } : { timeout: 5000 }); + url: args.url, + }); + + if (!session.data.ok) { throw Error('Unable to create state'); } @@ -359,24 +374,23 @@ export class TonhubConnector { getSessionState = async (sessionId: string): Promise => { return await backoff(async () => { - let ex = (await axios.get('https://connect.tonhubapi.com/connect/' + sessionId, this.adapter ? { timeout: 5000, adapter: this.adapter } : { timeout: 5000 })).data; - return this.ensureSessionStateCorrect(sessionId, ex); + let session = this.transport.call('session_get', { + id: sessionId + }); + return this.ensureSessionStateCorrect(sessionId, session); }); } waitForSessionState = async (sessionId: string, lastUpdated?: number): Promise => { return await backoff(async () => { - let params: AxiosRequestConfig = this.adapter ? { timeout: 30000, adapter: this.adapter } : { timeout: 30000 } - params.timeout = 30000 - params.params = { - lastUpdated - }; - let ex = (await axios.get('https://connect.tonhubapi.com/connect/' + sessionId + '/wait', params)).data; - return this.ensureSessionStateCorrect(sessionId, ex); + let session = this.transport.call('session_wait', { + id: sessionId + }); + return this.ensureSessionStateCorrect(sessionId, session); }) } - awaitSessionReady = async (sessionId: string, timeout: number, lastUpdated: number): Promise => { + awaitSessionReady = async (sessionId: string, timeout: number): Promise => { let expires = Date.now() + timeout; let res: TonhubSessionStateReady | TonhubSessionStateExpired | TonhubSessionStateRevoked = await backoff(async () => { while (Date.now() < expires) { @@ -459,7 +473,9 @@ export class TonhubConnector { let boc = pkg.toBoc({ idx: false }).toString('base64'); // Post command - await backoff(() => axios.post('https://connect.tonhubapi.com/connect/command', { job: boc }, this.adapter ? { timeout: 5000, adapter: this.adapter } : { timeout: 5000 })); + await backoff(() => this.transport.call('command_new', { + job: boc, + })); // Await result let result = await this._awaitJobState(request.appPublicKey, boc); @@ -522,8 +538,11 @@ export class TonhubConnector { .endCell(); let boc = pkg.toBoc({ idx: false }).toString('base64'); + // Post command - await backoff(() => axios.post('https://connect.tonhubapi.com/connect/command', { job: boc }, this.adapter ? { timeout: 5000, adapter: this.adapter } : { timeout: 5000 })); + await backoff(() => this.transport.call('command_new', { + job: boc, + })); // Await result let result = await this._awaitJobState(request.appPublicKey, boc); @@ -563,7 +582,9 @@ export class TonhubConnector { private _getJobState = async (appPublicKey: string, boc: string): Promise<{ type: 'expired' | 'rejected' | 'submitted' } | { type: 'completed', result: string }> => { let appk = toUrlSafe(appPublicKey); - let res = (await axios.get('https://connect.tonhubapi.com/connect/command/' + appk, this.adapter ? { timeout: 5000, adapter: this.adapter } : { timeout: 5000 })).data; + + let res = await this.transport.call('command_get', { appk }); + if (!jobStateCodec.is(res)) { throw Error('Invalid response from server'); } @@ -571,7 +592,7 @@ export class TonhubConnector { return { type: 'expired' }; } if (res.job !== boc) { - return { type: 'rejected' }; + return { type: 'rejected' }; } if (res.state === 'expired') { return { type: 'expired' }; diff --git a/src/transport/TonXTransport.ts b/src/transport/TonXTransport.ts new file mode 100644 index 0000000..69274da --- /dev/null +++ b/src/transport/TonXTransport.ts @@ -0,0 +1,25 @@ + +export class TonXTransport implements Transport { + call(method: string, args: TArgs): Promise { + let tonX = TonXTransport.getTonX(); + if (!tonX) { + throw new Error('ton-x not found'); + } + + return tonX.call(method, args); + } + + private static getTonX() { + if (!window || !(window as any).tonX) { + return null; + } + + return window && (window as any).tonX as { + call: (type: string, payload: TPayload) => Promise + }; + } + + static isAvailable() { + return this.getTonX() !== null; + } +} \ No newline at end of file diff --git a/src/transport/TonhubHttpTransport.ts b/src/transport/TonhubHttpTransport.ts new file mode 100644 index 0000000..705034e --- /dev/null +++ b/src/transport/TonhubHttpTransport.ts @@ -0,0 +1,87 @@ +import axios from 'axios'; + +export class TonhubHttpTransport implements Transport { + private readonly _endpoint: string; + private readonly _timeout: number; + + private adapter?: any; + + constructor(config?: { endpoint?: string, adapter?: any, timeout?: number }) { + this._endpoint = config?.endpoint || 'https://connect.tonhubapi.com'; + this._timeout = config?.timeout || 5000; + this.adapter = config?.adapter; + } + + private getAxiosConfig() { + return this.adapter ? { timeout: this._timeout, adapter: this.adapter } : { timeout: this._timeout }; + } + + call(method: string, args: any): Promise { + if (method === 'session_new') { + return this.createSession(args); + } else if (method === 'session_get') { + return this.getSession(args); + } else if (method === 'session_wait') { + return this.waitSession(args); + } else if (method === 'command_new') { + return this.createCommand(args); + } else if (method === 'command_get') { + return this.getCommand(args); + } else { + throw new Error('Unsupported method'); + } + } + + async createSession(args: any) { + let session = await axios.post( + `${this._endpoint}/connect/init`, + args, + this.getAxiosConfig(), + ); + if (!session.data.ok) { + throw Error('Unable to create session: ' + JSON.stringify(session.data)); + } + return session.data; + } + + async getSession(args: { id: string }) { + if (!args.id) { + throw new Error('Invalid session id'); + } + + let session = await axios.get( + `${this._endpoint}/connect/` + args.id, + this.getAxiosConfig() + ); + if (!session.data.ok) { + throw Error('Unable to create session: ' + JSON.stringify(session.data)); + } + return session.data; + } + + async waitSession(args: { id: string, lastUpdated?: number }) { + if (!args.id) { + throw new Error('Invalid session id'); + } + + let session = await axios.get( + `${this._endpoint}/connect/` + args.id + '/wait?lastUpdated='+(args.lastUpdated || 0), + this.getAxiosConfig() + ); + if (!session.data.ok) { + throw Error('Unable to create session: ' + JSON.stringify(session.data)); + } + return session.data; + } + + async createCommand(args: any) { + let result = await axios.post(`${this._endpoint}/connect/command`, args, this.getAxiosConfig()); + if (!result.data.ok) { + throw new Error('Cannot create command: ' + JSON.stringify(result.data)); + } + return result.data; + } + async getCommand(appk: string) { + return (await axios.get(`${this._endpoint}/connect/command/` + appk, this.getAxiosConfig())).data; + } +} \ No newline at end of file diff --git a/src/transport/Transport.ts b/src/transport/Transport.ts new file mode 100644 index 0000000..9e7158b --- /dev/null +++ b/src/transport/Transport.ts @@ -0,0 +1,3 @@ +interface Transport { + call(method: string, args: TArgs): Promise +} \ No newline at end of file From bf3690d1cc5f22668038e451a704f1a8da533147 Mon Sep 17 00:00:00 2001 From: mrmld Date: Tue, 7 Jun 2022 19:49:27 +0400 Subject: [PATCH 2/4] wip: rename TonXTransport to TonhubEmbeddedTransport --- src/connector/TonhubConnector.ts | 36 ++++++++++--------- ...ransport.ts => TonhubEmbeddedTransport.ts} | 8 ++--- 2 files changed, 23 insertions(+), 21 deletions(-) rename src/transport/{TonXTransport.ts => TonhubEmbeddedTransport.ts} (73%) diff --git a/src/connector/TonhubConnector.ts b/src/connector/TonhubConnector.ts index 3d0a363..503c4ea 100644 --- a/src/connector/TonhubConnector.ts +++ b/src/connector/TonhubConnector.ts @@ -8,7 +8,7 @@ import { Cell, Address, beginCell, CommentMessage, safeSign, contractAddress, sa import BN from 'bn.js'; import { WalletV4Source } from 'ton-contracts'; import { TonhubHttpTransport } from '../transport/TonhubHttpTransport'; -import { TonXTransport } from '../transport/TonXTransport'; +import { TonhubEmbeddedTransport } from '../transport/TonhubEmbeddedTransport'; const sessionStateCodec = t.union([ t.type({ @@ -169,8 +169,8 @@ function textToCell(src: string) { } function autodiscoverTransport(config?: { adapter: any }) { - if (TonXTransport.isAvailable()) { - return new TonXTransport(); + if (TonhubEmbeddedTransport.isAvailable()) { + return new TonhubEmbeddedTransport(); } return new TonhubHttpTransport({ adapter: config?.adapter, @@ -276,22 +276,23 @@ export class TonhubConnector { return signed; } - readonly testnet: boolean; + readonly network: 'mainnet' | 'sandbox'; readonly transport: Transport; - constructor(args?: { testnet?: boolean, adapter?: any, transport?: Transport }) { - let testnet = false; + constructor(args?: { network?: 'mainnet' | 'sandbox', adapter?: any, transport?: Transport }) { + let network: 'mainnet' | 'sandbox' = 'mainnet'; let adapter: any | null = null; if (args) { - if (typeof args.testnet === 'boolean') { - testnet = args.testnet; + if (args.network !== undefined) { + network = args.network; } if (args.adapter) { adapter = args.adapter; } } - this.testnet = testnet; + + this.network = network; this.transport = args?.transport || autodiscoverTransport({ adapter }); } @@ -306,7 +307,7 @@ export class TonhubConnector { await backoff(async () => { let session = await this.transport.call('session_new', { key: sessionId, - testnet: this.testnet, + testnet: this.network === 'sandbox', name: args.name, url: args.url, }); @@ -321,7 +322,7 @@ export class TonhubConnector { return { id: sessionId, seed: seed.toString('base64'), - link: (this.testnet ? 'ton-test://connect/' : 'ton://connect/') + sessionId + '?endpoint=connect.tonhubapi.com' + link: (this.network === 'sandbox' ? 'ton-test://connect/' : 'ton://connect/') + sessionId + '?endpoint=connect.tonhubapi.com' }; } @@ -330,7 +331,7 @@ export class TonhubConnector { throw Error('Invalid response from server'); } if (ex.state === 'initing') { - if (ex.testnet !== this.testnet) { + if (ex.testnet !== (this.network === 'sandbox')) { return { state: 'revoked' }; } return { @@ -345,7 +346,7 @@ export class TonhubConnector { if (ex.revoked) { return { state: 'revoked' }; } - if (ex.testnet !== this.testnet) { + if (ex.testnet !== (this.network === 'sandbox')) { return { state: 'revoked' }; } if (!TonhubConnector.verifyWalletConfig(sessionId, ex.wallet)) { @@ -384,17 +385,18 @@ export class TonhubConnector { waitForSessionState = async (sessionId: string, lastUpdated?: number): Promise => { return await backoff(async () => { let session = this.transport.call('session_wait', { - id: sessionId + id: sessionId, + lastUpdated }); return this.ensureSessionStateCorrect(sessionId, session); }) } - awaitSessionReady = async (sessionId: string, timeout: number): Promise => { + awaitSessionReady = async (sessionId: string, timeout: number, lastUpdated?: number): Promise => { let expires = Date.now() + timeout; let res: TonhubSessionStateReady | TonhubSessionStateExpired | TonhubSessionStateRevoked = await backoff(async () => { while (Date.now() < expires) { - let existing = await this.getSessionState(sessionId); + let existing = await this.waitForSessionState(sessionId, lastUpdated); if (existing.state !== 'initing') { if (existing.state === 'ready') { return existing; @@ -584,7 +586,7 @@ export class TonhubConnector { let appk = toUrlSafe(appPublicKey); let res = await this.transport.call('command_get', { appk }); - + if (!jobStateCodec.is(res)) { throw Error('Invalid response from server'); } diff --git a/src/transport/TonXTransport.ts b/src/transport/TonhubEmbeddedTransport.ts similarity index 73% rename from src/transport/TonXTransport.ts rename to src/transport/TonhubEmbeddedTransport.ts index 69274da..76783e1 100644 --- a/src/transport/TonXTransport.ts +++ b/src/transport/TonhubEmbeddedTransport.ts @@ -1,7 +1,7 @@ -export class TonXTransport implements Transport { +export class TonhubEmbeddedTransport implements Transport { call(method: string, args: TArgs): Promise { - let tonX = TonXTransport.getTonX(); + let tonX = TonhubEmbeddedTransport.get(); if (!tonX) { throw new Error('ton-x not found'); } @@ -9,7 +9,7 @@ export class TonXTransport implements Transport { return tonX.call(method, args); } - private static getTonX() { + private static get() { if (!window || !(window as any).tonX) { return null; } @@ -20,6 +20,6 @@ export class TonXTransport implements Transport { } static isAvailable() { - return this.getTonX() !== null; + return this.get() !== null; } } \ No newline at end of file From 554aeefd8c49a1b8ee74d522b55dea4327411d91 Mon Sep 17 00:00:00 2001 From: mrmld Date: Mon, 13 Jun 2022 18:23:12 +0300 Subject: [PATCH 3/4] fix: incorrect checks --- src/connector/TonhubConnector.ts | 6 +++--- src/transport/TonhubEmbeddedTransport.ts | 2 +- src/transport/TonhubHttpTransport.ts | 19 ++++--------------- 3 files changed, 8 insertions(+), 19 deletions(-) diff --git a/src/connector/TonhubConnector.ts b/src/connector/TonhubConnector.ts index 503c4ea..ba2f3ea 100644 --- a/src/connector/TonhubConnector.ts +++ b/src/connector/TonhubConnector.ts @@ -313,7 +313,7 @@ export class TonhubConnector { }); - if (!session.data.ok) { + if (!session.ok) { throw Error('Unable to create state'); } }); @@ -375,7 +375,7 @@ export class TonhubConnector { getSessionState = async (sessionId: string): Promise => { return await backoff(async () => { - let session = this.transport.call('session_get', { + let session = await this.transport.call('session_get', { id: sessionId }); return this.ensureSessionStateCorrect(sessionId, session); @@ -384,7 +384,7 @@ export class TonhubConnector { waitForSessionState = async (sessionId: string, lastUpdated?: number): Promise => { return await backoff(async () => { - let session = this.transport.call('session_wait', { + let session = await this.transport.call('session_wait', { id: sessionId, lastUpdated }); diff --git a/src/transport/TonhubEmbeddedTransport.ts b/src/transport/TonhubEmbeddedTransport.ts index 76783e1..79bb971 100644 --- a/src/transport/TonhubEmbeddedTransport.ts +++ b/src/transport/TonhubEmbeddedTransport.ts @@ -10,7 +10,7 @@ export class TonhubEmbeddedTransport implements Transport { } private static get() { - if (!window || !(window as any).tonX) { + if (typeof window === 'undefined' || !(window as any)?.tonX) { return null; } diff --git a/src/transport/TonhubHttpTransport.ts b/src/transport/TonhubHttpTransport.ts index 705034e..c59c463 100644 --- a/src/transport/TonhubHttpTransport.ts +++ b/src/transport/TonhubHttpTransport.ts @@ -1,4 +1,5 @@ import axios from 'axios'; +import { string } from 'fp-ts'; export class TonhubHttpTransport implements Transport { private readonly _endpoint: string; @@ -38,9 +39,6 @@ export class TonhubHttpTransport implements Transport { args, this.getAxiosConfig(), ); - if (!session.data.ok) { - throw Error('Unable to create session: ' + JSON.stringify(session.data)); - } return session.data; } @@ -53,9 +51,6 @@ export class TonhubHttpTransport implements Transport { `${this._endpoint}/connect/` + args.id, this.getAxiosConfig() ); - if (!session.data.ok) { - throw Error('Unable to create session: ' + JSON.stringify(session.data)); - } return session.data; } @@ -66,22 +61,16 @@ export class TonhubHttpTransport implements Transport { let session = await axios.get( `${this._endpoint}/connect/` + args.id + '/wait?lastUpdated='+(args.lastUpdated || 0), - this.getAxiosConfig() + { ...this.getAxiosConfig(), timeout: 30000 } ); - if (!session.data.ok) { - throw Error('Unable to create session: ' + JSON.stringify(session.data)); - } return session.data; } async createCommand(args: any) { let result = await axios.post(`${this._endpoint}/connect/command`, args, this.getAxiosConfig()); - if (!result.data.ok) { - throw new Error('Cannot create command: ' + JSON.stringify(result.data)); - } return result.data; } - async getCommand(appk: string) { - return (await axios.get(`${this._endpoint}/connect/command/` + appk, this.getAxiosConfig())).data; + async getCommand(args: { appk: string }) { + return (await axios.get(`${this._endpoint}/connect/command/` + args.appk, this.getAxiosConfig())).data; } } \ No newline at end of file From 92186a68eebf18cebc807885f2150c9c609c71a2 Mon Sep 17 00:00:00 2001 From: mrmld Date: Mon, 13 Jun 2022 18:30:13 +0300 Subject: [PATCH 4/4] wip: remove adapter from transport --- src/connector/TonhubConnector.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/connector/TonhubConnector.ts b/src/connector/TonhubConnector.ts index ba2f3ea..eaf40df 100644 --- a/src/connector/TonhubConnector.ts +++ b/src/connector/TonhubConnector.ts @@ -168,12 +168,11 @@ function textToCell(src: string) { return res; } -function autodiscoverTransport(config?: { adapter: any }) { +export function autodiscoverTransport() { if (TonhubEmbeddedTransport.isAvailable()) { return new TonhubEmbeddedTransport(); } return new TonhubHttpTransport({ - adapter: config?.adapter, endpoint: 'https://connect.tonhubapi.com' }); } @@ -280,20 +279,16 @@ export class TonhubConnector { readonly transport: Transport; - constructor(args?: { network?: 'mainnet' | 'sandbox', adapter?: any, transport?: Transport }) { + constructor(args?: { network?: 'mainnet' | 'sandbox', transport?: Transport }) { let network: 'mainnet' | 'sandbox' = 'mainnet'; - let adapter: any | null = null; if (args) { if (args.network !== undefined) { network = args.network; } - if (args.adapter) { - adapter = args.adapter; - } } this.network = network; - this.transport = args?.transport || autodiscoverTransport({ adapter }); + this.transport = args?.transport || autodiscoverTransport(); } createNewSession = async (args: { name: string, url: string }): Promise => {