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