Merge pull request #3 from ton-foundation/transport

draft: ton-x transport & separate from tonhub http api
main
Steve Korshakov 2022-06-14 15:25:15 +04:00 committed by GitHub
commit e5e6f261af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 156 additions and 34 deletions

View File

@ -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 { TonhubEmbeddedTransport } from '../transport/TonhubEmbeddedTransport';
const sessionStateCodec = t.union([
t.type({
@ -166,6 +168,15 @@ function textToCell(src: string) {
return res;
}
export function autodiscoverTransport() {
if (TonhubEmbeddedTransport.isAvailable()) {
return new TonhubEmbeddedTransport();
}
return new TonhubHttpTransport({
endpoint: 'https://connect.tonhubapi.com'
});
}
export class TonhubConnector {
static extractPublicKey(config: {
@ -264,22 +275,20 @@ export class TonhubConnector {
return signed;
}
readonly testnet: boolean;
readonly adapter: any | null;
readonly network: 'mainnet' | 'sandbox';
readonly transport: Transport;
constructor(args?: { testnet?: boolean, adapter?: any }) {
let testnet = false;
let adapter: any | null = null;
constructor(args?: { network?: 'mainnet' | 'sandbox', transport?: Transport }) {
let network: 'mainnet' | 'sandbox' = 'mainnet';
if (args) {
if (typeof args.testnet === 'boolean') {
testnet = args.testnet;
}
if (args.adapter) {
adapter = args.adapter;
if (args.network !== undefined) {
network = args.network;
}
}
this.testnet = testnet;
this.adapter = adapter;
this.network = network;
this.transport = args?.transport || autodiscoverTransport();
}
createNewSession = async (args: { name: string, url: string }): Promise<TonhubCreatedSession> => {
@ -291,13 +300,15 @@ 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,
testnet: this.network === 'sandbox',
name: args.name,
url: args.url
}, this.adapter ? { timeout: 5000, adapter: this.adapter } : { timeout: 5000 });
if (!session.data.ok) {
url: args.url,
});
if (!session.ok) {
throw Error('Unable to create state');
}
});
@ -306,7 +317,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'
};
}
@ -315,7 +326,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 {
@ -330,7 +341,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)) {
@ -359,28 +370,28 @@ export class TonhubConnector {
getSessionState = async (sessionId: string): Promise<TonhubSessionState> => {
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 = await this.transport.call('session_get', {
id: sessionId
});
return this.ensureSessionStateCorrect(sessionId, session);
});
}
waitForSessionState = async (sessionId: string, lastUpdated?: number): Promise<TonhubSessionState> => {
return await backoff(async () => {
let params: AxiosRequestConfig = this.adapter ? { timeout: 30000, adapter: this.adapter } : { timeout: 30000 }
params.timeout = 30000
params.params = {
let session = await this.transport.call('session_wait', {
id: sessionId,
lastUpdated
};
let ex = (await axios.get('https://connect.tonhubapi.com/connect/' + sessionId + '/wait', params)).data;
return this.ensureSessionStateCorrect(sessionId, ex);
});
return this.ensureSessionStateCorrect(sessionId, session);
})
}
awaitSessionReady = async (sessionId: string, timeout: number, lastUpdated: number): Promise<TonhubSessionAwaited> => {
awaitSessionReady = async (sessionId: string, timeout: number, lastUpdated?: number): Promise<TonhubSessionAwaited> => {
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;
@ -459,7 +470,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 +535,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 +579,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 +589,7 @@ export class TonhubConnector {
return { type: 'expired' };
}
if (res.job !== boc) {
return { type: 'rejected' };
return { type: 'rejected' };
}
if (res.state === 'expired') {
return { type: 'expired' };

View File

@ -0,0 +1,25 @@
export class TonhubEmbeddedTransport implements Transport {
call<TResult, TArgs>(method: string, args: TArgs): Promise<TResult> {
let tonX = TonhubEmbeddedTransport.get();
if (!tonX) {
throw new Error('ton-x not found');
}
return tonX.call(method, args);
}
private static get() {
if (typeof window === 'undefined' || !(window as any)?.tonX) {
return null;
}
return window && (window as any).tonX as {
call: <TResult, TPayload>(type: string, payload: TPayload) => Promise<TResult>
};
}
static isAvailable() {
return this.get() !== null;
}
}

View File

@ -0,0 +1,76 @@
import axios from 'axios';
import { string } from 'fp-ts';
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<any> {
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(),
);
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()
);
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(), timeout: 30000 }
);
return session.data;
}
async createCommand(args: any) {
let result = await axios.post(`${this._endpoint}/connect/command`, args, this.getAxiosConfig());
return result.data;
}
async getCommand(args: { appk: string }) {
return (await axios.get(`${this._endpoint}/connect/command/` + args.appk, this.getAxiosConfig())).data;
}
}

View File

@ -0,0 +1,3 @@
interface Transport {
call<TResult = any, TArgs = any>(method: string, args: TArgs): Promise<TResult>
}