feat: first version

This commit is contained in:
user committed 2024-05-26 19:36:56 +03:00
1 parent f907277998
commit 855b07f707
18 files changed
+7276 -1

No files matched your search

@@ -0,0 +1,6 @@
import { CompilerConfig } from '@ton/blueprint';
export const compile: CompilerConfig = {
lang: 'func',
targets: ['contracts/universal_wallet_contract.fc'],
};
+104
View File
@@ -0,0 +1,104 @@
import { Address, beginCell, Cell, Contract, contractAddress, ContractProvider, Sender, SendMode } from '@ton/core';
export type UniversalWalletContractConfig = {
subwalletId: number,
publicKey: Buffer,
admin: Address
};
export function universalWalletContractConfigToCell(config: UniversalWalletContractConfig): Cell {
if (config.publicKey.length !== 32) throw new Error('Public key must be 32 bytes long');
return beginCell()
.storeUint(config.subwalletId, 32)
.storeAddress(config.admin)
.storeRef(
beginCell()
.storeBuffer(config.publicKey)
.storeUint(0, 64)
.storeUint(0, 1)
.endCell()
)
.endCell();
}
export class UniversalWalletContract implements Contract {
constructor(readonly address: Address, readonly init?: { code: Cell; data: Cell }) {}
static createFromAddress(address: Address) {
return new UniversalWalletContract(address);
}
static createFromConfig(config: UniversalWalletContractConfig, code: Cell, workchain = 0) {
const data = universalWalletContractConfigToCell(config);
const init = { code, data };
return new UniversalWalletContract(contractAddress(workchain, init), init);
}
async sendDeploy(provider: ContractProvider, via: Sender, value: bigint) {
await provider.internal(via, {
value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell()
.endCell(),
});
}
async sendAdminProvideMessage(provider: ContractProvider, via: Sender, value: bigint, messages: {rawMsg: Cell, msgMode: number}[], queryId: number = 0) {
let messagesPacked = beginCell();
for (const {rawMsg, msgMode} of messages) {
messagesPacked = messagesPacked.storeUint(msgMode, 8).storeRef(rawMsg);
console.log(msgMode, rawMsg);
}
await provider.internal(via, {
value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell()
.storeUint(0xA173, 32)
.storeUint(queryId, 64)
.storeSlice(messagesPacked.endCell().beginParse())
.endCell(),
});
}
async sendSignedRequest(provider: ContractProvider, via: Sender, value: bigint, signedRequest: Cell, query_id: number = 0) {
await provider.internal(via, {
value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell()
.storeUint(0xA174, 32)
.storeSlice(signedRequest.beginParse())
.endCell(),
});
}
async sendEditCode(provider: ContractProvider, via: Sender, value: bigint, newCode: Cell, newData: Cell) {
await provider.internal(via, {
value,
sendMode: SendMode.PAY_GAS_SEPARATELY,
body: beginCell()
.storeUint(0xA183, 32)
.storeUint(0, 64)
.storeRef(newCode)
.storeRef(newData)
.endCell(),
});
}
async getWalletData(provider: ContractProvider) {
let stack = (await provider.get('get_wallet_data', [])).stack;
return {
subwalletId: stack.readNumber(),
admin: stack.readAddress()
}
}
async getMessageStates(provider: ContractProvider) {
let stack = (await provider.get('get_message_states', [])).stack;
return {
publicKey: stack.readBigNumber(),
stateCleaned: stack.readNumber(),
oldQueries: stack.readCellOpt()
};
}
}
+139
View File
@@ -0,0 +1,139 @@
import { sleep, NetworkProvider, UIProvider} from '@ton/blueprint';
import { Address, beginCell, Builder, Cell, Dictionary, DictionaryValue, Slice } from "@ton/core";
import { sha256 } from 'ton-crypto';
export const defaultJettonKeys = ["uri", "name", "description", "image", "image_data", "symbol", "decimals", "amount_style"];
export const defaultNftKeys = ["uri", "name", "description", "image", "image_data"];
export const promptBool = async (prompt:string, options:[string, string], ui:UIProvider, choice: boolean = false) => {
let yes = false;
let no = false;
let opts = options.map(o => o.toLowerCase());
do {
let res = (choice ? await ui.choose(prompt, options, (c: string) => c) : await ui.input(`${prompt}(${options[0]}/${options[1]})`)).toLowerCase();
yes = res == opts[0]
if(!yes)
no = res == opts[1];
} while(!(yes || no));
return yes;
}
export const promptAddress = async (prompt:string, provider:UIProvider, fallback?:Address) => {
let promptFinal = fallback ? prompt.replace(/:$/,'') + `(default:${fallback}):` : prompt ;
do {
let testAddr = (await provider.input(promptFinal)).replace(/^\s+|\s+$/g,'');
try{
return testAddr == "" && fallback ? fallback : Address.parse(testAddr);
}
catch(e) {
provider.write(testAddr + " is not valid!\n");
prompt = "Please try again:";
}
} while(true);
};
export const promptAmount = async (prompt:string, provider:UIProvider) => {
let resAmount:number;
do {
let inputAmount = await provider.input(prompt);
resAmount = Number(inputAmount);
if(isNaN(resAmount)) {
provider.write("Failed to convert " + inputAmount + " to float number");
}
else {
return resAmount.toFixed(9);
}
} while(true);
}
const keysToHashMap = async (keys: string[]) => {
let keyMap: {[key: string]: bigint} = {};
for (let i = 0; i < keys.length; i++) {
keyMap[keys[i]] = BigInt("0x" + (await sha256(keys[i])).toString('hex'));
}
}
const contentValue: DictionaryValue<string> = {
serialize: (src: string, builder:Builder) => {
builder.storeRef(beginCell().storeUint(0, 8).storeStringTail(src).endCell());
},
parse: (src: Slice) => {
const sc = src.loadRef().beginParse();
const prefix = sc.loadUint(8);
if(prefix == 0) {
return sc.loadStringTail();
}
else if(prefix == 1) {
// Not really tested, but feels like it should work
const chunkDict = Dictionary.loadDirect(Dictionary.Keys.Uint(32), Dictionary.Values.Cell(), sc);
return chunkDict.values().map(x => x.beginParse().loadStringTail()).join('');
}
else {
throw(Error(`Prefix ${prefix} is not supported yet`));
}
}
};
export const displayContentCell = async (content:Cell, ui:UIProvider, jetton:boolean = true, additional?: string[]) => {
const cs = content.beginParse();
const contentType = cs.loadUint(8);
if(contentType == 1) {
const noData = cs.remainingBits == 0;
if(noData && cs.remainingRefs == 0) {
ui.write("No data in content cell!\n");
}
else {
const contentUrl = noData ? cs.loadStringRefTail() : cs.loadStringTail();
ui.write(`Content metadata url:${contentUrl}\n`);
}
}
else if(contentType == 0) {
let contentKeys: string[];
const hasAdditional = additional !== undefined && additional.length > 0;
const contentDict = Dictionary.load(Dictionary.Keys.BigUint(256), contentValue, cs);
const contentMap : {[key: string]: string} = {};
if(jetton) {
contentKeys = hasAdditional ? [...defaultJettonKeys, ...additional] : defaultJettonKeys;
}
else {
contentKeys = hasAdditional ? [...defaultNftKeys, ...additional] : defaultNftKeys;
}
for (const name of contentKeys) {
// I know we should pre-compute hashed keys for known values... just not today.
const dictKey = BigInt("0x" + (await sha256(name)).toString('hex'))
const dictValue = contentDict.get(dictKey);
if(dictValue !== undefined) {
contentMap[name] = dictValue;
}
}
ui.write(`Content:${JSON.stringify(contentMap,null, 2)}`);
}
else {
ui.write(`Unknown content format indicator:${contentType}\n`);
}
}
export const promptUrl = async(prompt:string, ui:UIProvider) => {
let retry = false;
let input = "";
let res = "";
do {
input = await ui.input(prompt);
try{
let testUrl = new URL(input);
res = testUrl.toString();
retry = false;
}
catch(e) {
ui.write(input + " doesn't look like a valid url:\n" + e);
retry = !(await promptBool('Use anyway?(y/n)', ['y', 'n'], ui));
}
} while(retry);
return input;
}