init
This commit is contained in:
31
scripts/createWallet.ts
Normal file
31
scripts/createWallet.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { mnemonicNew, mnemonicToPrivateKey } from '@ton/crypto';
|
||||
import { WalletContractV4 } from '@ton/ton';
|
||||
import { mkdirSync, writeFileSync } from 'fs';
|
||||
|
||||
export async function createWallet() {
|
||||
let mnemonic = await mnemonicNew();
|
||||
let kp = await mnemonicToPrivateKey(mnemonic);
|
||||
let contract = WalletContractV4.create({
|
||||
workchain: 0,
|
||||
publicKey: kp.publicKey,
|
||||
});
|
||||
// Create wallets directory
|
||||
mkdirSync('wallets', { recursive: true });
|
||||
|
||||
console.log('Mnemonic:', mnemonic);
|
||||
console.log('Address:', contract.address.toString());
|
||||
// create file with wallet data in readable format
|
||||
const content = `WALLET_MNEMONIC="${mnemonic.join(' ')}"
|
||||
WALLET_VERSION="V4"
|
||||
WALLET_ADDRESS="${contract.address.toString()}"`;
|
||||
writeFileSync('wallets/wallet.env', content);
|
||||
console.log('Wallet data saved to wallets/wallet.env');
|
||||
return { mnemonic, contract };
|
||||
}
|
||||
|
||||
createWallet()
|
||||
.then(() => process.exit(0))
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
});
|
||||
32
scripts/crypto/crypto.test.ts
Normal file
32
scripts/crypto/crypto.test.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { randomBytes } from 'crypto';
|
||||
import { keyPairFromSeed } from '@ton/crypto';
|
||||
import { encryptContent, decryptContent } from './crypto';
|
||||
import { client, getKeypair, getWallet } from '../toolchain';
|
||||
import { WalletContractV4 } from '@ton/ton';
|
||||
|
||||
describe('Crypto functions', () => {
|
||||
test('encrypt and decrypt content', async () => {
|
||||
const sender = await getKeypair();
|
||||
const recipient = keyPairFromSeed(randomBytes(32));
|
||||
|
||||
const originalContent = 'Secret message';
|
||||
const encrypted = await encryptContent(sender.secretKey, recipient.publicKey, originalContent);
|
||||
const decrypted = await decryptContent(recipient.secretKey, sender.publicKey, encrypted);
|
||||
|
||||
expect(decrypted).toBe(originalContent);
|
||||
});
|
||||
|
||||
test('decrypt with wrong key fails', async () => {
|
||||
const senderSeed = Buffer.from(randomBytes(32));
|
||||
const recipientSeed = Buffer.from(randomBytes(32));
|
||||
const wrongRecipientSeed = Buffer.from(randomBytes(32));
|
||||
const sender = keyPairFromSeed(senderSeed);
|
||||
const recipient = await getKeypair();
|
||||
const wrongRecipient = keyPairFromSeed(wrongRecipientSeed);
|
||||
|
||||
const originalContent = 'Secret message';
|
||||
const encrypted = await encryptContent(sender.secretKey, recipient.publicKey, originalContent);
|
||||
|
||||
await expect(decryptContent(wrongRecipient.secretKey, sender.publicKey, encrypted)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
168
scripts/crypto/crypto.ts
Normal file
168
scripts/crypto/crypto.ts
Normal file
@@ -0,0 +1,168 @@
|
||||
import { keyPairFromSeed, keyPairFromSecretKey, sign, signVerify, KeyPair, getSecureRandomBytes } from '@ton/crypto';
|
||||
import nacl from 'tweetnacl';
|
||||
const ed2curve = require('ed2curve');
|
||||
import { client, getKeypair } from '../toolchain';
|
||||
import { Content } from '../../build/Gift/Gift_Gift';
|
||||
import { Address } from '@ton/core';
|
||||
import { WalletContractV4 } from '@ton/ton';
|
||||
|
||||
/**
|
||||
* Converts a bigint (uint256) to Buffer (32 bytes, big-endian).
|
||||
* @param bigIntValue The bigint value to convert.
|
||||
* @param byteLength The byte length (default 32 for uint256).
|
||||
* @returns Buffer The converted byte buffer.
|
||||
*/
|
||||
function bigIntToBuffer(bigIntValue: bigint, byteLength: number = 32): Buffer {
|
||||
const buffer = Buffer.alloc(byteLength);
|
||||
for (let i = 0; i < byteLength; i++) {
|
||||
buffer[byteLength - 1 - i] = Number((bigIntValue >> BigInt(i * 8)) & 0xffn);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the public key of the recipient wallet.
|
||||
* @param address The recipient's TON address.
|
||||
* @returns Promise<Buffer> The recipient's public key as Buffer.
|
||||
*/
|
||||
export async function getRecipientPublicKey(address: Address): Promise<Buffer> {
|
||||
console.log(`Fetching public key for: ${address.toString()}`);
|
||||
return client.callGetMethod(address, 'get_public_key')
|
||||
.then((result) => {
|
||||
return bigIntToBuffer(result.stack.readBigNumber());
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Error fetching recipient public key:', error);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a Content message using sender's private key and recipient's public key.
|
||||
* @param content The Content to encrypt.
|
||||
* @param senderSecretKey The sender's secret key.
|
||||
* @param recipientPublicKey The recipient's public key.
|
||||
* @returns Promise<Uint8Array> The encrypted message data (nonce + encrypted).
|
||||
*/
|
||||
export async function encryptMessage(
|
||||
content: Content,
|
||||
senderSecretKey: Buffer,
|
||||
recipientPublicKey: Buffer,
|
||||
): Promise<Uint8Array> {
|
||||
// Serialize Content to JSON string
|
||||
const contentString = JSON.stringify(content);
|
||||
const contentBuffer = new Uint8Array(Buffer.from(contentString, 'utf-8'));
|
||||
|
||||
// Convert Ed25519 keys to Curve25519
|
||||
const senderSecretCurve = ed2curve.convertSecretKey(senderSecretKey);
|
||||
const recipientPublicCurve = ed2curve.convertPublicKey(recipientPublicKey);
|
||||
if (!senderSecretCurve || !recipientPublicCurve) {
|
||||
throw new Error('Invalid key conversion');
|
||||
}
|
||||
|
||||
// Generate a random nonce
|
||||
const nonceBuffer = await getSecureRandomBytes(24);
|
||||
const nonce = new Uint8Array(nonceBuffer);
|
||||
|
||||
// Encrypt using NaCl box
|
||||
const encrypted = nacl.box(contentBuffer, nonce, recipientPublicCurve, senderSecretCurve);
|
||||
|
||||
// Return nonce + encrypted
|
||||
const combined = new Uint8Array(nonce.length + encrypted.length);
|
||||
combined.set(nonce, 0);
|
||||
combined.set(encrypted, nonce.length);
|
||||
return combined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt an encrypted message using recipient's private key and sender's public key.
|
||||
* @param encryptedData The encrypted data (nonce + encrypted).
|
||||
* @param recipientSecretKey The recipient's secret key.
|
||||
* @param senderPublicKey The sender's public key.
|
||||
* @returns Promise<Content> The decrypted Content.
|
||||
*/
|
||||
export async function decryptMessage(
|
||||
encryptedData: Uint8Array,
|
||||
recipientSecretKey: Buffer,
|
||||
senderPublicKey: Buffer,
|
||||
): Promise<Content> {
|
||||
// Extract nonce (first 24 bytes)
|
||||
const nonce = encryptedData.slice(0, 24);
|
||||
const encrypted = encryptedData.slice(24);
|
||||
|
||||
// Convert Ed25519 keys to Curve25519
|
||||
const recipientSecretCurve = ed2curve.convertSecretKey(recipientSecretKey);
|
||||
const senderPublicCurve = ed2curve.convertPublicKey(senderPublicKey);
|
||||
if (!recipientSecretCurve || !senderPublicCurve) {
|
||||
throw new Error('Invalid key conversion');
|
||||
}
|
||||
|
||||
// Decrypt using NaCl box open
|
||||
const decrypted = nacl.box.open(encrypted, nonce, senderPublicCurve, recipientSecretCurve);
|
||||
if (!decrypted) {
|
||||
throw new Error('Decryption failed');
|
||||
}
|
||||
|
||||
const contentString = new Buffer(decrypted).toString('utf-8');
|
||||
const content: Content = JSON.parse(contentString);
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt a string content using sender's private key and recipient's public key.
|
||||
* @param senderSecretKey The sender's secret key.
|
||||
* @param recipientPublicKey The recipient's public key.
|
||||
* @param content The string to encrypt.
|
||||
* @returns Promise<Uint8Array> The encrypted data.
|
||||
*/
|
||||
export async function encryptContent(
|
||||
senderSecretKey: Buffer,
|
||||
recipientPublicKey: Buffer,
|
||||
content: string,
|
||||
): Promise<Uint8Array> {
|
||||
const contentBuffer = new Uint8Array(Buffer.from(content, 'utf-8'));
|
||||
|
||||
// Convert Ed25519 keys to Curve25519
|
||||
const senderSecretCurve = ed2curve.convertSecretKey(senderSecretKey);
|
||||
const recipientPublicCurve = ed2curve.convertPublicKey(recipientPublicKey);
|
||||
if (!senderSecretCurve || !recipientPublicCurve) {
|
||||
throw new Error('Invalid key conversion');
|
||||
}
|
||||
|
||||
const nonceBuffer = await getSecureRandomBytes(24);
|
||||
const nonce = new Uint8Array(nonceBuffer);
|
||||
const encrypted = nacl.box(contentBuffer, nonce, recipientPublicCurve, senderSecretCurve);
|
||||
const combined = new Uint8Array(nonce.length + encrypted.length);
|
||||
combined.set(nonce, 0);
|
||||
combined.set(encrypted, nonce.length);
|
||||
return combined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Decrypt encrypted content using recipient's private key and sender's public key.
|
||||
* @param recipientSecretKey The recipient's secret key.
|
||||
* @param senderPublicKey The sender's public key.
|
||||
* @param encrypted The encrypted data.
|
||||
* @returns Promise<string> The decrypted string.
|
||||
*/
|
||||
export async function decryptContent(
|
||||
recipientSecretKey: Buffer,
|
||||
senderPublicKey: Buffer,
|
||||
encrypted: Uint8Array,
|
||||
): Promise<string> {
|
||||
// Convert Ed25519 keys to Curve25519
|
||||
const recipientSecretCurve = ed2curve.convertSecretKey(recipientSecretKey);
|
||||
const senderPublicCurve = ed2curve.convertPublicKey(senderPublicKey);
|
||||
if (!recipientSecretCurve || !senderPublicCurve) {
|
||||
throw new Error('Invalid key conversion');
|
||||
}
|
||||
|
||||
const nonce = encrypted.slice(0, 24);
|
||||
const encryptedPart = encrypted.slice(24);
|
||||
const decrypted = nacl.box.open(encryptedPart, nonce, senderPublicCurve, recipientSecretCurve);
|
||||
if (!decrypted) {
|
||||
throw new Error('Decryption failed');
|
||||
}
|
||||
return new Buffer(decrypted).toString('utf-8');
|
||||
}
|
||||
42
scripts/deployGift.ts
Normal file
42
scripts/deployGift.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { toNano, Address } from '@ton/core';
|
||||
import { Gift } from '../build/Gift/Gift_Gift';
|
||||
import { NetworkProvider } from '@ton/blueprint';
|
||||
import { mnemonicToPrivateKey } from '@ton/crypto';
|
||||
import { WalletContractV4 } from '@ton/ton';
|
||||
import { encryptMessage, getRecipientPublicKey } from './crypto/crypto';
|
||||
|
||||
export async function run(provider: NetworkProvider) {
|
||||
const ui = provider.ui();
|
||||
|
||||
let wallet = provider.sender();
|
||||
let kp = await mnemonicToPrivateKey(process.env.WALLET_MNEMONIC!!.split(' '));
|
||||
let walletContract = provider.open(
|
||||
WalletContractV4.create({
|
||||
workchain: provider.sender().address!!.workChain,
|
||||
publicKey: kp.publicKey,
|
||||
}),
|
||||
);
|
||||
|
||||
let receiverAddress = await ui.inputAddress('Receiver address');
|
||||
let text = await ui.input('Text content');
|
||||
let imageUrlString = null; // await ui.input('ImageUrl (optional)');
|
||||
|
||||
let content = { $$type: 'Content' as const, content: text, imageUrl: imageUrlString || null };
|
||||
let recipientPublicKey = await getRecipientPublicKey(receiverAddress);
|
||||
let encrypted = await encryptMessage(content, kp.secretKey, recipientPublicKey);
|
||||
let encryptedContent = {
|
||||
$$type: 'Content' as const,
|
||||
content: Buffer.from(encrypted).toString('base64'),
|
||||
imageUrl: null,
|
||||
};
|
||||
|
||||
const gift = provider.open(await Gift.fromInit(encryptedContent, receiverAddress));
|
||||
|
||||
await wallet.send({
|
||||
to: gift.address,
|
||||
value: toNano('0.05'),
|
||||
init: gift.init,
|
||||
});
|
||||
|
||||
await provider.waitForDeploy(gift.address);
|
||||
}
|
||||
104
scripts/toolchain.ts
Normal file
104
scripts/toolchain.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Address, toNano } from '@ton/core';
|
||||
import { Gift, Content, storeContent, storeNotification } from '../build/Gift/Gift_Gift';
|
||||
import { createNetworkProvider, NetworkProvider, sleep, UIProvider } from '@ton/blueprint';
|
||||
import { mnemonicToPrivateKey, sign } from '@ton/crypto';
|
||||
import { TonClient, TonClientParameters, WalletContractV4, beginCell } from '@ton/ton';
|
||||
import { signatureOf } from '@tact-lang/compiler';
|
||||
import { Sign } from 'crypto';
|
||||
import { decryptContent, encryptContent, getRecipientPublicKey } from './crypto/crypto';
|
||||
|
||||
export const client = new TonClient({
|
||||
endpoint: 'https://testnet.toncenter.com/api/v2/jsonRPC',
|
||||
});
|
||||
|
||||
export async function getKeypair() {
|
||||
let kp = await mnemonicToPrivateKey(process.env.WALLET_MNEMONIC!!.split(' '));
|
||||
return kp;
|
||||
}
|
||||
|
||||
export async function getWallet(client: TonClient) {
|
||||
let kp = await getKeypair();
|
||||
let walletContract = client.open(
|
||||
WalletContractV4.create({
|
||||
workchain: 0,
|
||||
publicKey: kp.publicKey,
|
||||
}),
|
||||
);
|
||||
return walletContract;
|
||||
}
|
||||
|
||||
export function myAddress() {
|
||||
return Address.parse(process.env.WALLET_ADDRESS!!);
|
||||
}
|
||||
|
||||
export async function deployGift(np: NetworkProvider, recipient: Address, content: string) {
|
||||
let kp = await getKeypair();
|
||||
let walletContract = await getWallet(client);
|
||||
let recipientPublicKey = await getRecipientPublicKey(recipient);
|
||||
let encryptedContent = await encryptContent(kp.secretKey, recipientPublicKey, content);
|
||||
|
||||
const gift = np.open(
|
||||
await Gift.fromInit(
|
||||
{ $$type: 'Content' as const, content: Buffer.from(encryptedContent).toString('base64') },
|
||||
recipient,
|
||||
),
|
||||
);
|
||||
|
||||
await np.sender().send({
|
||||
to: gift.address,
|
||||
value: toNano('0.05'),
|
||||
init: gift.init,
|
||||
});
|
||||
|
||||
// try 45 times with 2 seconds delay to get gift deployed
|
||||
for (let i = 0; i < 45; i++) {
|
||||
try {
|
||||
if ((await client.getContractState(gift.address)).state == 'active') {
|
||||
return gift;
|
||||
}
|
||||
} catch (e) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
}
|
||||
}
|
||||
throw new Error('Gift deployment failed after multiple attempts');
|
||||
}
|
||||
|
||||
export async function getListOfGifts(address: Address) {
|
||||
let notificationHash = beginCell()
|
||||
.store(storeNotification({ $$type: 'Notification' }))
|
||||
.endCell()
|
||||
.hash();
|
||||
let listOfGifts = await client
|
||||
.getTransactions(address, {
|
||||
limit: 50,
|
||||
})
|
||||
.then((transactions) =>
|
||||
transactions.filter(
|
||||
(tx) =>
|
||||
tx.inMessage != null && tx.inMessage?.body.hash().equals(notificationHash) && tx.inMessage.info.src,
|
||||
),
|
||||
);
|
||||
return listOfGifts.map((tx) => {
|
||||
let giftContractAddress = tx.inMessage!.info.src! as Address;
|
||||
return {
|
||||
gift: Gift.fromAddress(giftContractAddress),
|
||||
address: giftContractAddress,
|
||||
time: tx.now || 0,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export async function readGift(gift: Gift) {
|
||||
let delay = 1000;
|
||||
let contentMethod = await client.runMethod(gift.address, 'content');
|
||||
await sleep(delay);
|
||||
let content = contentMethod.stack.readString() || '';
|
||||
let senderMethod = await client.runMethod(gift.address, 'owner');
|
||||
await sleep(delay);
|
||||
let senderAddress = senderMethod.stack.readAddress();
|
||||
let senderPublicKey = await getRecipientPublicKey(senderAddress);
|
||||
await sleep(delay);
|
||||
console.log(`Gift content (encrypted): ${content}`);
|
||||
await sleep(delay);
|
||||
return decryptContent((await getKeypair()).secretKey, senderPublicKey, Buffer.from(content, 'base64'));
|
||||
}
|
||||
90
scripts/tui.ts
Normal file
90
scripts/tui.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { NetworkProvider, sleep, UIProvider } from '@ton/blueprint';
|
||||
import { encryptContent, getRecipientPublicKey } from './crypto/crypto';
|
||||
import { getKeypair, deployGift, getListOfGifts, readGift, myAddress, client } from './toolchain';
|
||||
import { Address } from '@ton/core';
|
||||
|
||||
function checkEnv() {
|
||||
if (!process.env.WALLET_MNEMONIC) {
|
||||
throw new Error('WALLET_MNEMONIC is not set in environment variables');
|
||||
}
|
||||
if (!process.env.WALLET_ADDRESS) {
|
||||
throw new Error('WALLET_ADDRESS is not set in environment variables');
|
||||
}
|
||||
if (process.env.WALLET_VERSION !== 'V4') {
|
||||
throw new Error('WALLET_VERSION must be set to V4 in environment variables');
|
||||
}
|
||||
}
|
||||
|
||||
export async function run(provider: NetworkProvider) {
|
||||
const ui = provider.ui();
|
||||
checkEnv();
|
||||
|
||||
while (true) {
|
||||
const choice = await ui.choose('Select an option:', ['Create gift', 'Receive gift', 'Exit'], (c) => c);
|
||||
switch (choice) {
|
||||
case 'Create gift':
|
||||
await handleCreateGift(ui);
|
||||
break;
|
||||
case 'Receive gift':
|
||||
await handleReceiveGift(ui);
|
||||
break;
|
||||
case 'Exit':
|
||||
ui.write('Goodbye!');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function handleCreateGift(ui: UIProvider) {
|
||||
const text = await ui.input('Enter text to send:');
|
||||
const recipientAddress = await ui.inputAddress(
|
||||
'Enter recipient TON address:',
|
||||
Address.parse('0QDl9pZ0wzHiYCKxyzdbR5WN6kAVTS2Ggra-Fx-O182Ms47d'),
|
||||
);
|
||||
ui.write(`Original text: ${text}`);
|
||||
|
||||
try {
|
||||
const senderKp = await getKeypair();
|
||||
const recipientPublicKey = await getRecipientPublicKey(recipientAddress);
|
||||
const encrypted = await encryptContent(senderKp.secretKey, recipientPublicKey, text);
|
||||
ui.write(`Encrypted text: ${Buffer.from(encrypted).toString('base64')}`);
|
||||
|
||||
// Placeholder for sending
|
||||
ui.setActionPrompt('Sending to blockchain...');
|
||||
// create gift here
|
||||
|
||||
await deployGift(provider, recipientAddress, text);
|
||||
|
||||
ui.clearActionPrompt();
|
||||
ui.write('Gift sent successfully!');
|
||||
} catch (error) {
|
||||
ui.write(`Error: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleReceiveGift(ui: any) {
|
||||
try {
|
||||
const gifts = await getListOfGifts(myAddress());
|
||||
if (gifts.length === 0) {
|
||||
ui.write('No gifts found.');
|
||||
await sleep(2000);
|
||||
return;
|
||||
}
|
||||
const choices = gifts;
|
||||
const selectedGift = await ui.choose('Select a gift:', choices, (g: any) => {
|
||||
const date = new Date(g.time * 1000);
|
||||
const timeStr = date.toTimeString().slice(0, 5); // HH:MM
|
||||
const dateStr = date.toLocaleDateString('en-GB'); // dd/mm/yyyy, but we need dd-mm-yy
|
||||
const dayMonthYear = `${date.getDate().toString().padStart(2, '0')}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getFullYear().toString().slice(-2)}`;
|
||||
const addressStr = g.gift?.address?.toString() || 'Unknown';
|
||||
return `${timeStr} ${dayMonthYear} ${addressStr}`;
|
||||
});
|
||||
ui.write(`Selected gift: ${selectedGift.address?.toString() || 'Unknown'}`);
|
||||
ui.setActionPrompt('Decrypting gift...');
|
||||
const content = await readGift(selectedGift.gift);
|
||||
ui.clearActionPrompt();
|
||||
ui.write(`Decrypted text: ${content}`);
|
||||
} catch (error) {
|
||||
ui.write(`Error: ${(error as Error).message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user