|
| 1 | +import { Address, beginCell, Cell, contractAddress, Dictionary, DictionaryValue, Slice, StateInit } from '@ton/core' |
| 2 | +import { NftFixPriceSaleV4R1CodeCell } from './NftFixPriceSaleV4.source' |
| 3 | +import { sign, mnemonicNew,mnemonicToPrivateKey } from '@ton/crypto' |
| 4 | +import { bufferToInt, toBufferBE } from "../../ton/bigint"; |
| 5 | + |
| 6 | +export type JettonPriceType = { |
| 7 | + price: bigint, |
| 8 | + jettonMaster: Address, |
| 9 | +} |
| 10 | + |
| 11 | +export type NftFixPriceSaleV4DR1Data = { |
| 12 | + isComplete: boolean |
| 13 | + createdAt: number |
| 14 | + marketplaceAddress: Address |
| 15 | + nftAddress: Address |
| 16 | + nftOwnerAddress: Address | null |
| 17 | + fullTonPrice: bigint |
| 18 | + marketplaceFeeAddress: Address |
| 19 | + marketplaceFeePercent: number |
| 20 | + royaltyAddress: Address |
| 21 | + royaltyPercent: number |
| 22 | + soldAtTime: number, |
| 23 | + soldQueryId: bigint, |
| 24 | +} |
| 25 | + |
| 26 | +export function buildFixPriceV4SaleData(input: Pick<NftFixPriceSaleV4DR1Data, 'nftAddress'|'royaltyAddress'|'royaltyPercent'|'fullTonPrice'|'nftOwnerAddress'>): Omit<NftFixPriceSaleV4DR1Data, 'marketplaceAddress'> { |
| 27 | + return { |
| 28 | + ...input, |
| 29 | + isComplete: false, |
| 30 | + createdAt: Math.floor(Date.now()/1000), |
| 31 | + marketplaceFeeAddress: Address.parse('EQDDuxx7sa3Dt2GE85a0sIHp4GVoa7OKbAanfo3co9H-h06d'), |
| 32 | + marketplaceFeePercent: 0.05, |
| 33 | + soldAtTime: 0, |
| 34 | + soldQueryId: 0n, |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +export const OP_FIX_PRICE_V4_DEPLOY_JETTON = 0xfb5dbf47 |
| 39 | +export const OP_FIX_PRICE_V4_DEPLOY_BLANK = 0x664c0905 |
| 40 | +export const OP_FIX_PRICE_V4_CHANGE_PRICE = 0xfd135f7b |
| 41 | + |
| 42 | +export const NftFixPriceJettonPriceValue: DictionaryValue<JettonPriceType> = { |
| 43 | + serialize(src, builder) { |
| 44 | + builder.storeCoins(src.price) |
| 45 | + .storeAddress(src.jettonMaster) |
| 46 | + }, |
| 47 | + parse(slice) { |
| 48 | + return { |
| 49 | + price: slice.loadCoins(), |
| 50 | + jettonMaster: slice.loadAddress(), |
| 51 | + } |
| 52 | + }, |
| 53 | +} |
| 54 | + |
| 55 | +function assertPercent(x: number) { |
| 56 | + if (x < 0) { |
| 57 | + throw new Error(`Percent can not be less zero, got ${x}`) |
| 58 | + } |
| 59 | + if (x > 1) { |
| 60 | + throw new Error('Percent should be less than one') |
| 61 | + } |
| 62 | + const p = Number((x * 100_000).toFixed(0)) |
| 63 | + if (!Number.isInteger(p) || p < 0) { |
| 64 | + throw new Error(`Percent should be integer after multiple 100k ${x} -> ${p}`) |
| 65 | + } |
| 66 | + return p |
| 67 | +} |
| 68 | + |
| 69 | +async function randomKeyPair() { |
| 70 | + const mnemonics = await mnemonicNew() |
| 71 | + return mnemonicToPrivateKey(mnemonics) |
| 72 | +} |
| 73 | + |
| 74 | +export function buildNftFixPriceSaleV4R1Data(cfg: NftFixPriceSaleV4DR1Data & {publicKey: Buffer | null}) { |
| 75 | + return beginCell() |
| 76 | + .storeBit(cfg.isComplete) |
| 77 | + .storeAddress(cfg.marketplaceAddress) |
| 78 | + .storeAddress(cfg.nftOwnerAddress) |
| 79 | + .storeCoins(cfg.fullTonPrice) |
| 80 | + .storeUint(cfg.soldAtTime, 32) |
| 81 | + .storeUint(cfg.soldQueryId, 64) |
| 82 | + .storeRef(beginCell() |
| 83 | + .storeAddress(cfg.marketplaceFeeAddress) |
| 84 | + .storeAddress(cfg.royaltyAddress) |
| 85 | + .storeUint(assertPercent(cfg.marketplaceFeePercent), 17) |
| 86 | + .storeUint(assertPercent(cfg.royaltyPercent), 17) |
| 87 | + .storeAddress(cfg.nftAddress) |
| 88 | + .storeUint(cfg.createdAt, 32) |
| 89 | + .endCell()) |
| 90 | + .storeDict(undefined) // empty jetton dict |
| 91 | + .storeMaybeBuffer(cfg.publicKey, 256 / 8) |
| 92 | + .endCell() |
| 93 | +} |
| 94 | + |
| 95 | +export function parseNftFixPriceSaleV4R1Data(slice: Slice) { |
| 96 | + const isComplete = slice.loadBit() |
| 97 | + const marketplaceAddress = slice.loadMaybeAddress() |
| 98 | + const nftOwnerAddress = slice.loadMaybeAddress() |
| 99 | + const fullTonPrice = slice.loadCoins() |
| 100 | + const soldAtTime = slice.loadUint(32) |
| 101 | + const soldQueryId = slice.loadUintBig(64) |
| 102 | + const staticData = slice.loadRef().beginParse() |
| 103 | + const marketplaceFeeAddress = staticData.loadMaybeAddress() |
| 104 | + const royaltyAddress = staticData.loadMaybeAddress() |
| 105 | + const marketplaceFeePercent = staticData.loadUint(17) |
| 106 | + const royaltyPercent = staticData.loadUint(17) |
| 107 | + const nftAddress = staticData.loadMaybeAddress() |
| 108 | + const createdAt = staticData.loadUint(32) |
| 109 | + const jettonPrice = slice.loadDict(Dictionary.Keys.BigUint(256), NftFixPriceJettonPriceValue) |
| 110 | + const publicKey = slice.remainingBits > 0 ? slice.loadMaybeUintBig(256) : null |
| 111 | + return { |
| 112 | + isComplete, |
| 113 | + createdAt, |
| 114 | + marketplaceAddress, |
| 115 | + nftAddress, |
| 116 | + nftOwnerAddress, |
| 117 | + fullTonPrice, |
| 118 | + marketplaceFeeAddress, |
| 119 | + marketplaceFeePercent: marketplaceFeePercent / 100_000, |
| 120 | + royaltyAddress, |
| 121 | + royaltyPercent: royaltyPercent / 100_000, |
| 122 | + jettonPrice, |
| 123 | + soldAtTime, |
| 124 | + soldQueryId, |
| 125 | + publicKey: publicKey ? toBufferBE(publicKey, 256 / 8) : null, |
| 126 | + } |
| 127 | +} |
| 128 | + |
| 129 | +export type NftFixPriceSaleV4DR1DataRelaxed = ReturnType<typeof parseNftFixPriceSaleV4R1Data> |
| 130 | + |
| 131 | +export function isValidFixPriceSaleV4DR1Data(input: NftFixPriceSaleV4DR1DataRelaxed): input is (NftFixPriceSaleV4DR1Data & { |
| 132 | + jettonPrice: Dictionary<bigint, JettonPriceType>, |
| 133 | + publicKey: Buffer|null |
| 134 | +}) { |
| 135 | + if (!input.marketplaceAddress) { |
| 136 | + throw new Error('marketplaceAddress is null') |
| 137 | + } |
| 138 | + if (!input.nftAddress) { |
| 139 | + throw new Error('nftAddress is null') |
| 140 | + } |
| 141 | + if (!input.marketplaceFeeAddress) { |
| 142 | + throw new Error('marketplaceFeeAddress is null') |
| 143 | + } |
| 144 | + if (!input.royaltyAddress) { |
| 145 | + throw new Error('royaltyAddress is null') |
| 146 | + } |
| 147 | + |
| 148 | + if (input.marketplaceAddress.workChain !== 0) { |
| 149 | + throw new Error('marketplaceAddress wrong workchain') |
| 150 | + } |
| 151 | + if (input.nftAddress.workChain !== 0) { |
| 152 | + throw new Error('nftAddress wrong workchain') |
| 153 | + } |
| 154 | + if (input.marketplaceFeeAddress.workChain !== 0) { |
| 155 | + throw new Error('marketplaceFeeAddress wrong workchain') |
| 156 | + } |
| 157 | + if (input.royaltyAddress.workChain !== 0) { |
| 158 | + throw new Error('royaltyAddress wrong workchain') |
| 159 | + } |
| 160 | + if (input.nftOwnerAddress && input.nftOwnerAddress.workChain !== 0) { |
| 161 | + throw new Error('nftOwnerAddress wrong workchain') |
| 162 | + } |
| 163 | + |
| 164 | + if (input.marketplaceFeePercent < 0 || input.marketplaceFeePercent > 1 || isNaN(input.marketplaceFeePercent)) { |
| 165 | + throw new Error('marketplaceFeePercent wrong value') |
| 166 | + } |
| 167 | + if (input.royaltyPercent < 0 || input.royaltyPercent > 1 || isNaN(input.royaltyPercent)) { |
| 168 | + throw new Error('royaltyPercent wrong value') |
| 169 | + } |
| 170 | + |
| 171 | + if ((input.marketplaceFeePercent + input.royaltyPercent) > 1) { |
| 172 | + throw new Error('fee to big') |
| 173 | + } |
| 174 | + return true |
| 175 | +} |
| 176 | + |
| 177 | +export function nftFixPriceV4CreateDeployMessage(queryId: bigint, marketplaceAddress: Address, jettonPrice: Dictionary<bigint, JettonPriceType>, secretKey: Buffer) { |
| 178 | + const signedData = beginCell() |
| 179 | + .storeAddress(marketplaceAddress) |
| 180 | + .storeDict(jettonPrice) |
| 181 | + .endCell() |
| 182 | + const signature = sign(signedData.hash(), secretKey) |
| 183 | + return beginCell() |
| 184 | + .storeUint(OP_FIX_PRICE_V4_DEPLOY_JETTON, 32) |
| 185 | + .storeUint(queryId, 64) |
| 186 | + .storeBuffer(signature, 512 / 8) |
| 187 | + .storeAddress(marketplaceAddress) |
| 188 | + .storeDict(jettonPrice) |
| 189 | + .endCell() |
| 190 | +} |
| 191 | + |
| 192 | +export function nftFixPriceV4ChangePriceMessage(opts: { |
| 193 | + queryId: bigint, |
| 194 | + newTonPrice: bigint, |
| 195 | + newJettonPrice: Dictionary<bigint, JettonPriceType> | null, |
| 196 | +}) { |
| 197 | + return beginCell() |
| 198 | + .storeUint(OP_FIX_PRICE_V4_CHANGE_PRICE, 32) |
| 199 | + .storeUint(opts.queryId, 64) |
| 200 | + .storeCoins(opts.newTonPrice) |
| 201 | + .storeDict(opts.newJettonPrice) |
| 202 | + .endCell() |
| 203 | +} |
| 204 | + |
| 205 | +export async function buildJettonPriceDict(opts: { |
| 206 | + saleAddress: Address, |
| 207 | + jettonPrices: { [key: string]: bigint }, // jettonMasterWallet:price |
| 208 | + jettonWalletAddressResolver: (jettonMaster: Address, address: Address) => Promise<Address> |
| 209 | +}) { |
| 210 | + const jettonPricesTags = Object.keys(opts.jettonPrices) |
| 211 | + const jettonPriceDict = Dictionary.empty(Dictionary.Keys.BigUint(256), NftFixPriceJettonPriceValue) |
| 212 | + for (const jettonMasterStr of jettonPricesTags) { |
| 213 | + const jettonMaster = Address.parse(jettonMasterStr) |
| 214 | + const jettonWalletAddress = await opts.jettonWalletAddressResolver(jettonMaster, opts.saleAddress) |
| 215 | + if (jettonWalletAddress.workChain !== 0) { |
| 216 | + throw new Error(`Jetton address has wrong workchain ${jettonWalletAddress.workChain} for jetton ${jettonMasterStr}`) |
| 217 | + } |
| 218 | + const price = opts.jettonPrices[jettonMasterStr] |
| 219 | + if (!price || price <= 0n) { |
| 220 | + throw new Error(`Wrong jetton price for jetton ${jettonMasterStr}, ${price}`) |
| 221 | + } |
| 222 | + jettonPriceDict.set(bufferToInt(jettonWalletAddress.hash), { |
| 223 | + jettonMaster, |
| 224 | + price, |
| 225 | + }) |
| 226 | + } |
| 227 | + return jettonPriceDict |
| 228 | +} |
| 229 | + |
| 230 | +export async function buildNftFixPriceSaleV4R1DeployData(opts: { |
| 231 | + codeCell?: Cell, |
| 232 | + queryId: bigint, |
| 233 | + deployerAddress: Address, |
| 234 | + marketplaceAddress: Address, |
| 235 | + config: Omit<NftFixPriceSaleV4DR1Data, 'marketplaceAddress'>, |
| 236 | + jettonPrices: { [key: string]: bigint }, // jettonMasterWallet:price |
| 237 | + jettonWalletAddressResolver: (jettonMaster: Address, address: Address) => Promise<Address> |
| 238 | +}) { |
| 239 | + const keypair = await randomKeyPair() |
| 240 | + const dataCell = buildNftFixPriceSaleV4R1Data({ |
| 241 | + ...opts.config, |
| 242 | + publicKey: keypair.publicKey, |
| 243 | + marketplaceAddress: opts.deployerAddress, |
| 244 | + }) |
| 245 | + const si: StateInit = { |
| 246 | + code: opts.codeCell ?? NftFixPriceSaleV4R1CodeCell, |
| 247 | + data: dataCell, |
| 248 | + } |
| 249 | + const saleAddress = contractAddress(0, si) |
| 250 | + const jettonPriceDict = await buildJettonPriceDict({ |
| 251 | + saleAddress, |
| 252 | + jettonPrices: opts.jettonPrices, |
| 253 | + jettonWalletAddressResolver: opts.jettonWalletAddressResolver, |
| 254 | + }) |
| 255 | + return { |
| 256 | + address: saleAddress, |
| 257 | + stateInit: si, |
| 258 | + message: nftFixPriceV4CreateDeployMessage(opts.queryId, opts.marketplaceAddress, jettonPriceDict, keypair.secretKey), |
| 259 | + } |
| 260 | +} |
0 commit comments