ref: extract signature verify, moved documentation
parent
37b6390602
commit
bc330b0ca6
134
README.md
134
README.md
|
@ -3,142 +3,16 @@
|
|||
Connector for dApps for TON blockchain
|
||||
|
||||
Supported connectors:
|
||||
* [Tonhub](https://tonhub.com) and [Tondev](https://test.tonhub.com)
|
||||
* [Tonhub](https://tonhub.com) and [Sandbox Wallet](https://test.tonhub.com)
|
||||
* Extension for [Tonhub](https://tonhub.com) and [Sandbox Wallet](https://test.tonhub.com)
|
||||
|
||||
```
|
||||
yarn install ton-x
|
||||
```
|
||||
|
||||
## Tonhub connector
|
||||
## Documentation
|
||||
|
||||
Connecting app to a wallet:
|
||||
1) Create session
|
||||
2) Show user QR Code or link
|
||||
3) Await session confirmation
|
||||
|
||||
### Create connector
|
||||
|
||||
```typescript
|
||||
import { TonhubConnector } from 'ton-x';
|
||||
const connector = new TonhubConnector({ testnet: true });
|
||||
```
|
||||
|
||||
### Creating session
|
||||
|
||||
```typescript
|
||||
let session: TonhubCreatedSession = await connector.createNewSession({
|
||||
name: 'Your app name',
|
||||
url: 'Your app url'
|
||||
});
|
||||
|
||||
// Session ID, Seed and Auth Link
|
||||
const sessionId = session.id;
|
||||
const sessionSeed = session.seed;
|
||||
const sessionLink = session.link;
|
||||
```
|
||||
|
||||
### Await session confirmation
|
||||
|
||||
```typescript
|
||||
const session: TonhubSessionAwaited = await connector.awaitSessionReady(sessionId, 5 * 60 * 1000); // 5 min timeout
|
||||
|
||||
if (session.state === 'revoked' || session.state === 'expired') {
|
||||
// Handle revoked or expired session
|
||||
} else if (session.state === 'ready') {
|
||||
|
||||
// Handle session
|
||||
const walletConfig: TonhubWalletConfig = session.walletConfig;
|
||||
|
||||
// You need to persist this values to work with this connection:
|
||||
// * sessionId
|
||||
// * sessionSeed
|
||||
// * walletConfig
|
||||
|
||||
// You can check signed wallet config on backend using TonhubConnector.verifyWalletConfig.
|
||||
// walletConfig is cryptographically signed for specific session and other parameters
|
||||
// you can safely use it as authentication proof without the need to sign something.
|
||||
const correctConfig: boolean = TonhubConnector.verifyWalletConfig(sessionId, walletConfig);
|
||||
|
||||
// ...
|
||||
|
||||
} else {
|
||||
throw new Error('Impossible');
|
||||
}
|
||||
```
|
||||
|
||||
### Check session validity
|
||||
After session established it is useful to check from time to time that session wasn't revoked
|
||||
|
||||
```typescript
|
||||
const session: TonhubSessionState = await connector.getSessionState(sessionId);
|
||||
```
|
||||
|
||||
### Request transaction
|
||||
|
||||
```typescript
|
||||
// Request body
|
||||
const request: TonhubTransactionRequest = {
|
||||
seed: sessionSeed, // Session Seed
|
||||
appPublicKey: walletConfig.appPublicKey, // Wallet's app public key
|
||||
to: 'EQCkR1cGmnsE45N4K0otPl5EnxnRakmGqeJUNua5fkWhales', // Destination
|
||||
value: '10000000000', // Amount in nano-tons
|
||||
timeout: 5 * 60 * 1000, // 5 minut timeout
|
||||
stateInit: '....', // Optional serialized to base64 string state_init cell
|
||||
text: 'Hello world', // Optional comment. If no payload specified - sends actual content, if payload is provided this text is used as UI-only hint
|
||||
payload: '....' // Optional serialized to base64 string payload cell
|
||||
};
|
||||
const response: TonhubTransactionResponse = await connector.requestTransaction(request);
|
||||
if (response.type === 'rejected') {
|
||||
// Handle rejection
|
||||
} else if (response.type === 'expired') {
|
||||
// Handle expiration
|
||||
} else if (response.type === 'invalid_session') {
|
||||
// Handle expired or invalid session
|
||||
} else if (response.type === 'success') {
|
||||
// Handle successful transaction
|
||||
const externalMessage = response.response; // Signed external message that was sent to the network
|
||||
} else {
|
||||
throw new Error('Impossible');
|
||||
}
|
||||
```
|
||||
|
||||
### Request signature
|
||||
In tonhub user user signs both text and binary payload, but only text is presented to the user. For future compatibility we recommend to prefix your cell binary payload with `0x00000000`.
|
||||
```typescript
|
||||
|
||||
const payloadToSign = Buffer.concat([Buffer.from([0, 0, 0, 0]), Buffer.from('Some random string')]);
|
||||
const payload = beginCell()
|
||||
.storeBuffer(payloadToSign)
|
||||
.endCell()
|
||||
.toBoc({idx:false})
|
||||
.toString('base64');
|
||||
const text = 'Please, sign our terms or service and privacy policy';
|
||||
|
||||
// Request body
|
||||
const request: TonhubSignRequest = {
|
||||
seed: sessionSeed, // Session Seed
|
||||
appPublicKey: walletConfig.appPublicKey, // Wallet's app public key
|
||||
timeout: 5 * 60 * 1000, // 5 minut timeout
|
||||
text: 'Hello world', // Text to sign, presented to the user.
|
||||
payload: payload // Optional serialized to base64 string payload cell
|
||||
};
|
||||
const response: TonhubSignResponse = await connector.requestSign(request);
|
||||
if (response.type === 'rejected') {
|
||||
// Handle rejection
|
||||
} else if (response.type === 'expired') {
|
||||
// Handle expiration
|
||||
} else if (response.type === 'invalid_session') {
|
||||
// Handle expired or invalid session
|
||||
} else if (response.type === 'success') {
|
||||
// Handle successful transaction
|
||||
const signature = response.signature;
|
||||
|
||||
// You can check signature on the backend with TonhubConnector.verifySignatureResponse
|
||||
let correctSignature = TonhubConnector.verifySignatureResponse({ signature: signature, config: walletConfig });
|
||||
} else {
|
||||
throw new Error('Impossible');
|
||||
}
|
||||
```
|
||||
All documentation moved to https://developers.tonhub.com
|
||||
|
||||
# License
|
||||
MIT
|
|
@ -7,6 +7,8 @@ import { Cell, Address, beginCell, CommentMessage, safeSign, contractAddress, sa
|
|||
import BN from 'bn.js';
|
||||
import { TonhubHttpTransport } from '../transport/TonhubHttpTransport';
|
||||
import { WalletV4Source } from "../contracts/WalletV4Source";
|
||||
import { extractPublicKeyAndAddress } from "../contracts/extractPublicKeyAndAddress";
|
||||
import { verifySignatureResponse } from "./crypto";
|
||||
|
||||
const sessionStateCodec = t.union([
|
||||
t.type({
|
||||
|
@ -168,32 +170,13 @@ function textToCell(src: string) {
|
|||
|
||||
export class TonhubConnector {
|
||||
|
||||
static extractPublicKey(config: {
|
||||
walletType: string,
|
||||
walletConfig: string
|
||||
}) {
|
||||
// Extract public key and address
|
||||
let publicKey: Buffer;
|
||||
let restoredAddress: Address;
|
||||
if (config.walletType === 'org.ton.wallets.v4') {
|
||||
let source = WalletV4Source.restore(config.walletConfig);
|
||||
restoredAddress = contractAddress(source);
|
||||
publicKey = source.publicKey;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Public key
|
||||
return { publicKey, address: restoredAddress };
|
||||
}
|
||||
|
||||
static verifyWalletConfig(session: string, config: TonhubWalletConfig) {
|
||||
|
||||
// Check address
|
||||
const address = Address.parseFriendly(config.address).address;
|
||||
|
||||
// Extract public key and address
|
||||
let extracted = TonhubConnector.extractPublicKey(config);
|
||||
let extracted = extractPublicKeyAndAddress(config);
|
||||
if (!extracted) {
|
||||
return false;
|
||||
}
|
||||
|
@ -225,45 +208,6 @@ export class TonhubConnector {
|
|||
return safeSignVerify(toSign, Buffer.from(config.walletSig, 'base64'), publicKey);
|
||||
}
|
||||
|
||||
static verifySignatureResponse(args: {
|
||||
signature: string,
|
||||
text?: string | null | undefined,
|
||||
payload?: string | null | undefined,
|
||||
config: TonhubWalletConfig
|
||||
}) {
|
||||
// Check address
|
||||
const address = Address.parseFriendly(args.config.address).address;
|
||||
|
||||
// Extract public key and address
|
||||
let extracted = TonhubConnector.extractPublicKey(args.config);
|
||||
if (!extracted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check address
|
||||
if (!extracted.address.equals(address)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let publicKey: Buffer = extracted.publicKey;
|
||||
|
||||
// Package
|
||||
const textCell = new Cell();
|
||||
const payloadCell = new Cell();
|
||||
if (typeof args.text === 'string') {
|
||||
new CommentMessage(args.text).writeTo(textCell);
|
||||
}
|
||||
|
||||
// Check signature
|
||||
const data = beginCell()
|
||||
.storeRef(textCell)
|
||||
.storeRef(payloadCell)
|
||||
.endCell();
|
||||
const signed = safeSignVerify(data, Buffer.from(args.signature, 'base64'), publicKey);
|
||||
|
||||
return signed;
|
||||
}
|
||||
|
||||
readonly network: 'mainnet' | 'sandbox';
|
||||
readonly transport: Transport;
|
||||
|
||||
|
@ -536,7 +480,7 @@ export class TonhubConnector {
|
|||
const cellRes = Cell.fromBoc(Buffer.from(result.result, 'base64'))[0];
|
||||
let slice = cellRes.beginParse();
|
||||
const resSignature = slice.readBuffer(64);
|
||||
let correct = TonhubConnector.verifySignatureResponse({ signature: resSignature.toString('base64'), config: session.wallet });
|
||||
let correct = verifySignatureResponse({ signature: resSignature.toString('base64'), config: session.wallet });
|
||||
if (correct) {
|
||||
return { type: 'success', signature: resSignature.toString('base64') };
|
||||
} else {
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import { Address, beginCell, Cell, CommentMessage, safeSignVerify } from "ton";
|
||||
import { extractPublicKeyAndAddress } from "../contracts/extractPublicKeyAndAddress";
|
||||
|
||||
export function verifySignatureResponse(args: {
|
||||
signature: string,
|
||||
text?: string | null | undefined,
|
||||
payload?: string | null | undefined,
|
||||
config: { address: string, walletType: string, walletConfig: string }
|
||||
}) {
|
||||
// Check address
|
||||
const address = Address.parseFriendly(args.config.address).address;
|
||||
|
||||
// Extract public key and address
|
||||
let extracted = extractPublicKeyAndAddress(args.config);
|
||||
if (!extracted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check address
|
||||
if (!extracted.address.equals(address)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let publicKey: Buffer = extracted.publicKey;
|
||||
|
||||
// Package
|
||||
const textCell = new Cell();
|
||||
const payloadCell = new Cell();
|
||||
if (typeof args.text === 'string') {
|
||||
new CommentMessage(args.text).writeTo(textCell);
|
||||
}
|
||||
|
||||
// Check signature
|
||||
const data = beginCell()
|
||||
.storeRef(textCell)
|
||||
.storeRef(payloadCell)
|
||||
.endCell();
|
||||
const signed = safeSignVerify(data, Buffer.from(args.signature, 'base64'), publicKey);
|
||||
|
||||
return signed;
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
import { Address, contractAddress } from "ton";
|
||||
import { WalletV4Source } from "./WalletV4Source";
|
||||
|
||||
export function extractPublicKeyAndAddress(config: {
|
||||
walletType: string,
|
||||
walletConfig: string
|
||||
}) {
|
||||
// Extract public key and address
|
||||
let publicKey: Buffer;
|
||||
let restoredAddress: Address;
|
||||
if (config.walletType === 'org.ton.wallets.v4') {
|
||||
let source = WalletV4Source.restore(config.walletConfig);
|
||||
restoredAddress = contractAddress(source);
|
||||
publicKey = source.publicKey;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Public key
|
||||
return { publicKey, address: restoredAddress };
|
||||
}
|
10
src/index.ts
10
src/index.ts
|
@ -19,4 +19,12 @@ export {
|
|||
TonhubLocalConfig,
|
||||
TonhubLocalTransactionRequest,
|
||||
TonhubLocalTransactionResponse
|
||||
} from './connector/TonhubLocalConnector';
|
||||
} from './connector/TonhubLocalConnector';
|
||||
|
||||
export {
|
||||
verifySignatureResponse
|
||||
} from './connector/crypto';
|
||||
|
||||
export {
|
||||
extractPublicKeyAndAddress
|
||||
} from './contracts/extractPublicKeyAndAddress';
|
Loading…
Reference in New Issue