Skip to content

Commit

Permalink
feat: support ton sign Proof
Browse files Browse the repository at this point in the history
  • Loading branch information
ByteZhang1024 committed Sep 3, 2024
1 parent accecd4 commit 9ee416f
Show file tree
Hide file tree
Showing 20 changed files with 1,131 additions and 104 deletions.
3 changes: 3 additions & 0 deletions packages/example/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -36,3 +36,6 @@ yarn-error.log*

# typescript
*.tsbuildinfo

# msw
public/mockServiceWorker.js
89 changes: 89 additions & 0 deletions packages/example/components/chains/ton/TonProofDemoApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {
Account,
ConnectAdditionalRequest,
TonProofItemReplySuccess,
} from '@tonconnect/ui-react';
import '../../../lib/localStorageForGithubPages';

class TonProofDemoApiService {
private localStorageKey = 'demo-api-access-token';

private host = window.location.origin;

public accessToken: string | null = null;

public readonly refreshIntervalMs = 9 * 60 * 1000;

constructor() {
this.accessToken = localStorage.getItem(this.localStorageKey);

if (!this.accessToken) {
this.generatePayload();
}
}

async generatePayload(): Promise<ConnectAdditionalRequest | null> {
try {
const response = await (
await fetch(`${this.host}/api/generate_payload`, {
method: 'POST',
})
).json();
return { tonProof: response.payload as string };
} catch {
return null;
}
}

async checkProof(proof: TonProofItemReplySuccess['proof'], account: Account): Promise<boolean> {
try {
const reqBody = {
address: account.address,
network: account.chain,
public_key: account.publicKey,
proof: {
...proof,
state_init: account.walletStateInit,
},
};

const response = await (
await fetch(`${this.host}/api/check_proof`, {
method: 'POST',
body: JSON.stringify(reqBody),
})
).json();

if (response?.token) {
localStorage.setItem(this.localStorageKey, response.token);
this.accessToken = response.token;
return true;
}
return false;
} catch (e) {
console.log('checkProof error:', e);
throw e;
}
}

async getAccountInfo(account: Account) {
const response = await (
await fetch(`${this.host}/api/get_account_info`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
},
})
).json();

return response as {};
}

reset() {
this.accessToken = null;
localStorage.removeItem(this.localStorageKey);
this.generatePayload();
}
}

export const TonProofDemoApi = new TonProofDemoApiService();
66 changes: 65 additions & 1 deletion packages/example/components/chains/ton/example.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from '@tonconnect/ui-react';
import InfoLayout from '../../InfoLayout';
import params from './params';
import { TonProofDemoApi } from './TonProofDemoApi';

export function Example() {
const userFriendlyAddress = useTonAddress();
Expand All @@ -33,6 +34,69 @@ export function Example() {
)}
</InfoLayout>

<ApiGroup title='Sign Proof 按步骤操作'>
<ApiPayload
title="步骤1: Loading Proof Data"
description="步骤1: 断开连接,生成 Proof Payload"
allowCallWithoutProvider={true}
onExecute={async (request: string) => {
// 断开连接,重置 accessToken
if (tonConnectUI.connected) {
await tonConnectUI?.disconnect();
TonProofDemoApi.reset();
}

// 设置 loading 状态
tonConnectUI.setConnectRequestParameters({ state: 'loading' });

const payload = await TonProofDemoApi.generatePayload();
if (payload) {
tonConnectUI.setConnectRequestParameters({ state: 'ready', value: payload });
} else {
tonConnectUI.setConnectRequestParameters(null);
}

return payload;
}}
/>
<ApiPayload
title="步骤2: 连接 Wallet"
description="步骤2: 连接 Wallet"
allowCallWithoutProvider={true}
onExecute={async (request: string) => {
void tonConnectUI.openModal();
return 'success';
}}
onValidate={async (request: string, response: string) => {
if (wallet.connectItems?.tonProof && 'proof' in wallet.connectItems.tonProof) {
try {
const result = await TonProofDemoApi.checkProof(wallet.connectItems.tonProof.proof, wallet.account);
return JSON.stringify({
success: result,
proof: wallet.connectItems.tonProof.proof
});
} catch (e: any) {
return JSON.stringify({
success: false,
errorMessage: e?.message,
proof: null
});
}
}
return Promise.resolve('error');
}}
/>
<ApiPayload
title="步骤3: 测试获取 Account Info"
description="步骤3: 测试获取 Account Info"
allowCallWithoutProvider={!!wallet}
onExecute={async (request: string) => {
const response = await TonProofDemoApi.getAccountInfo(wallet.account);
return response;
}}
/>
</ApiGroup>

<ApiGroup title="Send Transaction">
<ApiPayload
title="sendTransaction"
Expand Down Expand Up @@ -64,7 +128,7 @@ export function Example() {
export default function App() {
return (
<TonConnectUIProvider manifestUrl="https://dapp-example.onekeytest.com/tonconnect-manifest.json">
{/* <TonConnectUIProvider manifestUrl="https://ton-connect.github.io/demo-dapp-with-react-ui/tonconnect-manifest.json"> */}
{/* <TonConnectUIProvider manifestUrl="https://ton-connect.github.io/demo-dapp-with-react-ui/tonconnect-manifest.json"> */}
<Example />
</TonConnectUIProvider>
);
Expand Down
4 changes: 2 additions & 2 deletions packages/example/components/chains/ton/params.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export default {
},
{
id: 'sendTransaction-native-two',
name: 'Native',
name: 'Native Two',
value: JSON.stringify({
messages: [
{
Expand All @@ -30,7 +30,7 @@ export default {
},
{
id: 'sendTransaction-native-four',
name: 'Native',
name: 'Native Four',
value: JSON.stringify({
messages: [
{
Expand Down
20 changes: 20 additions & 0 deletions packages/example/lib/localStorageForGithubPages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
const separator = `${window.location.pathname.replace(/\/+$/, '')}:`;

const setItem = localStorage.setItem;
localStorage.constructor.prototype.setItem = (key: string, value: string) =>
setItem.apply(localStorage, [separator + key, value]);
localStorage.setItem = (key: string, value: string) =>
setItem.apply(localStorage, [separator + key, value]);

const getItem = localStorage.getItem;
localStorage.constructor.prototype.getItem = (key: string) =>
getItem.apply(localStorage, [separator + key]);
localStorage.getItem = (key: string) => getItem.apply(localStorage, [separator + key]);

const removeItem = localStorage.removeItem;
localStorage.constructor.prototype.removeItem = (key: string) =>
removeItem.apply(localStorage, [separator + key]);
localStorage.removeItem = (key: string) => removeItem.apply(localStorage, [separator + key]);

export {};
10 changes: 10 additions & 0 deletions packages/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@
"@starcoin/starcoin": "^2.1.6",
"@starcoin/starmask-onboarding": "^1.0.0",
"@tanstack/react-query": "^5.40.0",
"@ton/core": "^0.57.0",
"@ton/crypto": "^3.3.0",
"@ton/ton": "^15.0.0",
"@tonconnect/ui-react": "^2.0.6",
"@uiw/react-codemirror": "^4.22.1",
"@unisat/wallet-utils": "^1.0.0",
Expand All @@ -74,11 +77,13 @@
"console-feed": "^3.5.0",
"cosmjs-types": "^0.9.0",
"ethereumjs-util": "^7.1.5",
"jose": "^5.8.0",
"js-conflux-sdk": "^2.1.8",
"lodash-es": "^4.17.21",
"long": "^5.2.3",
"lucid-cardano": "^0.10.7",
"lucide-react": "^0.378.0",
"msw": "^2.4.1",
"near-api-js": "^0.44.2",
"next": "^13.5.6",
"next-themes": "^0.3.0",
Expand Down Expand Up @@ -111,5 +116,10 @@
"sass": "^1.51.0",
"tailwindcss": "^3.4.3",
"typescript": "5.1.6"
},
"msw": {
"workerDirectory": [
"public"
]
}
}
3 changes: 3 additions & 0 deletions packages/example/pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ import '../styles/globals.css';
import type { AppProps } from 'next/app';
import { ThemeProvider } from 'next-themes';
import { Toaster } from '../components/ui/toaster';
import MswProvider from '../server/MswProvider';

function MyApp({ Component, pageProps }: AppProps) {
return (
<ThemeProvider>
<MswProvider>
<Component {...pageProps} />
</MswProvider>
<Toaster />
</ThemeProvider>
);
Expand Down
21 changes: 21 additions & 0 deletions packages/example/server/MswProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
'use client';

import { handlers } from './worker';
import { useEffect, useState } from 'react';

export default function MswProvider({ children }: { children: React.ReactNode }) {
const [mocksReady, setMocksReady] = useState(false);

useEffect(() => {
if (typeof window !== 'undefined') {
void import('msw/browser').then((a) => {
void a
.setupWorker(...handlers(window.location.origin))
.start()
.then(() => setMocksReady(true));
});
}
}, []);

return mocksReady && children;
}
36 changes: 36 additions & 0 deletions packages/example/server/api/checkProof.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { HttpResponseResolver } from 'msw';
import { CheckProofRequest } from '../dto/checkProofRequestDto';
import { TonApiService } from '../services/tonApiService';
import { TonProofService } from '../services/tonProofService';
import { badRequest, ok } from '../utils/httpUtils';
import { createAuthToken, verifyToken } from '../utils/jwt';

/**
* Checks the proof and returns an access token.
*
* POST /api/check_proof
*/
export const checkProof: HttpResponseResolver = async ({ request }) => {
try {
const body = CheckProofRequest.parse(await request.json());

const client = TonApiService.create(body.network);
const service = new TonProofService();

const isValid = await service.checkProof(body, (address) => client.getWalletPublicKey(address));
if (!isValid) {
return badRequest({ error: 'Invalid proof' });
}

const payloadToken = body.proof.payload;
if (!(await verifyToken(payloadToken))) {
return badRequest({ error: 'Invalid token' });
}

const token = await createAuthToken({ address: body.address, network: body.network });

return ok({ token: token });
} catch (e) {
return badRequest({ error: 'Invalid request', trace: e });
}
};
22 changes: 22 additions & 0 deletions packages/example/server/api/generatePayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { HttpResponseResolver } from 'msw';
import { TonProofService } from '../services/tonProofService';
import { badRequest, ok } from '../utils/httpUtils';
import { createPayloadToken } from '../utils/jwt';

/**
* Generates a payload for ton proof.
*
* POST /api/generate_payload
*/
export const generatePayload: HttpResponseResolver = async () => {
try {
const service = new TonProofService();

const payload = service.generatePayload();
const payloadToken = await createPayloadToken({ payload: payload });

return ok({ payload: payloadToken });
} catch (e) {
return badRequest({ error: 'Invalid request', trace: e });
}
};
30 changes: 30 additions & 0 deletions packages/example/server/api/getAccountInfo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { HttpResponseResolver } from 'msw';
import { TonApiService } from '../services/tonApiService';
import { badRequest, ok, unauthorized } from '../utils/httpUtils';
import { decodeAuthToken, verifyToken } from '../utils/jwt';

/**
* Returns account info.
*
* GET /api/get_account_info
*/
export const getAccountInfo: HttpResponseResolver = async ({ request }) => {
try {
const token = request.headers.get('Authorization')?.replace('Bearer ', '');

if (!token || !(await verifyToken(token))) {
return unauthorized({ error: 'Unauthorized' });
}

const payload = decodeAuthToken(token);
if (!payload?.address || !payload?.network) {
return unauthorized({ error: 'Invalid token' });
}

const client = TonApiService.create(payload.network);

return ok(await client.getAccountInfo(payload.address));
} catch (e) {
return badRequest({ error: 'Invalid request', trace: e });
}
};
Loading

0 comments on commit 9ee416f

Please sign in to comment.