init
This commit is contained in:
3
.env.example
Normal file
3
.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
WALLET_MNEMONIC=""
|
||||
WALLET_VERSION="V4"
|
||||
WALLET_ADDRESS=""
|
||||
25
.gitignore
vendored
Normal file
25
.gitignore
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
node_modules
|
||||
temp
|
||||
build
|
||||
dist
|
||||
.DS_Store
|
||||
package.ts
|
||||
wallets/
|
||||
temp/*
|
||||
|
||||
# VS Code
|
||||
.vscode/*
|
||||
.history/
|
||||
*.vsix
|
||||
.env
|
||||
# IDEA files
|
||||
.idea
|
||||
|
||||
# VIM
|
||||
Session.vim
|
||||
.vim/
|
||||
|
||||
# Other private editor folders
|
||||
.nvim/
|
||||
.emacs/
|
||||
.helix/
|
||||
1
.prettierignore
Normal file
1
.prettierignore
Normal file
@@ -0,0 +1 @@
|
||||
build
|
||||
7
.prettierrc
Normal file
7
.prettierrc
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"printWidth": 120,
|
||||
"tabWidth": 4,
|
||||
"singleQuote": true,
|
||||
"bracketSpacing": true,
|
||||
"semi": true
|
||||
}
|
||||
26
README.md
Normal file
26
README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# SmartGift
|
||||
|
||||
## Project structure
|
||||
|
||||
- `contracts` - source code of all the smart contracts of the project and their dependencies.
|
||||
- `wrappers` - wrapper classes (implementing `Contract` from ton-core) for the contracts, including any [de]serialization primitives and compilation functions.
|
||||
- `tests` - tests for the contracts.
|
||||
- `scripts` - scripts used by the project, mainly the deployment scripts.
|
||||
|
||||
## How to use
|
||||
|
||||
### Build
|
||||
|
||||
`npx blueprint build` or `yarn blueprint build`
|
||||
|
||||
### Test
|
||||
|
||||
`npx blueprint test` or `yarn blueprint test`
|
||||
|
||||
### Deploy or run another script
|
||||
|
||||
`npx blueprint run` or `yarn blueprint run`
|
||||
|
||||
### Add a new contract
|
||||
|
||||
`npx blueprint create ContractName` or `yarn blueprint create ContractName`
|
||||
12
jest.config.ts
Normal file
12
jest.config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { Config } from 'jest';
|
||||
|
||||
const config: Config = {
|
||||
preset: 'ts-jest',
|
||||
globalSetup: './jest.setup.ts',
|
||||
cache: false, // disabled caching to prevent old Tact files from being used after a rebuild
|
||||
testEnvironment: '@ton/sandbox/jest-environment',
|
||||
testPathIgnorePatterns: ['/node_modules/', '/dist/'],
|
||||
reporters: ['default', ['@ton/sandbox/jest-reporter', {}]],
|
||||
};
|
||||
|
||||
export default config;
|
||||
7
jest.setup.ts
Normal file
7
jest.setup.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { buildAllTact } from '@ton/blueprint';
|
||||
import * as dotenv from 'dotenv';
|
||||
|
||||
export default async function () {
|
||||
dotenv.config(); // Load environment variables from .env
|
||||
await buildAllTact();
|
||||
}
|
||||
7876
package-lock.json
generated
Normal file
7876
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "SmartGift",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"bp": "blueprint",
|
||||
"start": "blueprint run",
|
||||
"build": "blueprint build",
|
||||
"test": "jest --verbose",
|
||||
"tui": "cd tui && npm start",
|
||||
"createWallet": "npx ts-node scripts/createWallet.ts",
|
||||
"release": "blueprint pack && npm publish --access public"
|
||||
},
|
||||
"dependencies": {
|
||||
"@opentui/core": "^0.1.86",
|
||||
"@ton/core": "~0",
|
||||
"@types/readline-sync": "^1.4.8",
|
||||
"buffer": "^6.0.3",
|
||||
"ed2curve": "^0.3.0",
|
||||
"readline-sync": "^1.4.10",
|
||||
"tweetnacl": "^1.0.3",
|
||||
"tweetnacl-util": "^0.15.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tact-lang/compiler": ">=1.6.13 <2.0.0",
|
||||
"@ton-community/func-js": ">=0.10.0",
|
||||
"@ton/blueprint": ">=0.40.0",
|
||||
"@ton/crypto": "^3.3.0",
|
||||
"@ton/sandbox": ">=0.37.0",
|
||||
"@ton/test-utils": ">=0.11.0",
|
||||
"@ton/tolk-js": ">=1.0.0",
|
||||
"@ton/ton": ">=15.3.1 <16.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^22.17.2",
|
||||
"dotenv": "^17.3.1",
|
||||
"jest": "^30.0.5",
|
||||
"prettier": "^3.6.2",
|
||||
"ts-jest": "^29.4.1",
|
||||
"ts-node": "^10.9.2",
|
||||
"typescript": "^5.9.2"
|
||||
}
|
||||
}
|
||||
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}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
15
tact.config.json
Normal file
15
tact.config.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/tact-lang/tact/main/src/config/configSchema.json",
|
||||
"projects": [
|
||||
{
|
||||
"name": "Gift",
|
||||
"path": "contracts/gift.tact",
|
||||
"output": "build/Gift",
|
||||
"options": {
|
||||
"debug": false,
|
||||
"external": false
|
||||
},
|
||||
"mode": "full"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
tests/Gift.spec.ts
Normal file
41
tests/Gift.spec.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Blockchain, SandboxContract, TreasuryContract } from '@ton/sandbox';
|
||||
import { toNano } from '@ton/core';
|
||||
import { Gift } from '../build/Gift/Gift_Gift';
|
||||
import '@ton/test-utils';
|
||||
|
||||
describe('Gift', () => {
|
||||
let blockchain: Blockchain;
|
||||
let deployer: SandboxContract<TreasuryContract>;
|
||||
let gift: SandboxContract<Gift>;
|
||||
|
||||
beforeEach(async () => {
|
||||
blockchain = await Blockchain.create();
|
||||
|
||||
deployer = await blockchain.treasury('deployer');
|
||||
|
||||
const content = { $$type: 'Content' as const, content: 'test', imageUrl: null };
|
||||
const receiver = deployer.address;
|
||||
|
||||
gift = blockchain.openContract(await Gift.fromInit(content, receiver));
|
||||
|
||||
const deployResult = await gift.send(
|
||||
deployer.getSender(),
|
||||
{
|
||||
value: toNano('0.05'),
|
||||
},
|
||||
null,
|
||||
);
|
||||
|
||||
expect(deployResult.transactions).toHaveTransaction({
|
||||
from: deployer.address,
|
||||
to: gift.address,
|
||||
deploy: true,
|
||||
success: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should deploy', async () => {
|
||||
// the check is done inside beforeEach
|
||||
// blockchain and gift are ready to use
|
||||
});
|
||||
});
|
||||
12
tsconfig.json
Normal file
12
tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2020",
|
||||
"outDir": "dist",
|
||||
"module": "commonjs",
|
||||
"declaration": true,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user