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
|
Connector for dApps for TON blockchain
|
||||||
|
|
||||||
Supported connectors:
|
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
|
yarn install ton-x
|
||||||
```
|
```
|
||||||
|
|
||||||
## Tonhub connector
|
## Documentation
|
||||||
|
|
||||||
Connecting app to a wallet:
|
All documentation moved to https://developers.tonhub.com
|
||||||
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');
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# License
|
# License
|
||||||
MIT
|
MIT
|
|
@ -7,6 +7,8 @@ import { Cell, Address, beginCell, CommentMessage, safeSign, contractAddress, sa
|
||||||
import BN from 'bn.js';
|
import BN from 'bn.js';
|
||||||
import { TonhubHttpTransport } from '../transport/TonhubHttpTransport';
|
import { TonhubHttpTransport } from '../transport/TonhubHttpTransport';
|
||||||
import { WalletV4Source } from "../contracts/WalletV4Source";
|
import { WalletV4Source } from "../contracts/WalletV4Source";
|
||||||
|
import { extractPublicKeyAndAddress } from "../contracts/extractPublicKeyAndAddress";
|
||||||
|
import { verifySignatureResponse } from "./crypto";
|
||||||
|
|
||||||
const sessionStateCodec = t.union([
|
const sessionStateCodec = t.union([
|
||||||
t.type({
|
t.type({
|
||||||
|
@ -168,32 +170,13 @@ function textToCell(src: string) {
|
||||||
|
|
||||||
export class TonhubConnector {
|
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) {
|
static verifyWalletConfig(session: string, config: TonhubWalletConfig) {
|
||||||
|
|
||||||
// Check address
|
// Check address
|
||||||
const address = Address.parseFriendly(config.address).address;
|
const address = Address.parseFriendly(config.address).address;
|
||||||
|
|
||||||
// Extract public key and address
|
// Extract public key and address
|
||||||
let extracted = TonhubConnector.extractPublicKey(config);
|
let extracted = extractPublicKeyAndAddress(config);
|
||||||
if (!extracted) {
|
if (!extracted) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -225,45 +208,6 @@ export class TonhubConnector {
|
||||||
return safeSignVerify(toSign, Buffer.from(config.walletSig, 'base64'), publicKey);
|
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 network: 'mainnet' | 'sandbox';
|
||||||
readonly transport: Transport;
|
readonly transport: Transport;
|
||||||
|
|
||||||
|
@ -536,7 +480,7 @@ export class TonhubConnector {
|
||||||
const cellRes = Cell.fromBoc(Buffer.from(result.result, 'base64'))[0];
|
const cellRes = Cell.fromBoc(Buffer.from(result.result, 'base64'))[0];
|
||||||
let slice = cellRes.beginParse();
|
let slice = cellRes.beginParse();
|
||||||
const resSignature = slice.readBuffer(64);
|
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) {
|
if (correct) {
|
||||||
return { type: 'success', signature: resSignature.toString('base64') };
|
return { type: 'success', signature: resSignature.toString('base64') };
|
||||||
} else {
|
} 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,
|
TonhubLocalConfig,
|
||||||
TonhubLocalTransactionRequest,
|
TonhubLocalTransactionRequest,
|
||||||
TonhubLocalTransactionResponse
|
TonhubLocalTransactionResponse
|
||||||
} from './connector/TonhubLocalConnector';
|
} from './connector/TonhubLocalConnector';
|
||||||
|
|
||||||
|
export {
|
||||||
|
verifySignatureResponse
|
||||||
|
} from './connector/crypto';
|
||||||
|
|
||||||
|
export {
|
||||||
|
extractPublicKeyAndAddress
|
||||||
|
} from './contracts/extractPublicKeyAndAddress';
|
Loading…
Reference in New Issue