Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
JavaScript
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758375937603761376237633764376537663767376837693770377137723773377437753776377737783779378037813782378337843785378637873788378937903791379237933794379537963797379837993800380138023803380438053806380738083809381038113812381338143815381638173818381938203821382238233824382538263827382838293830383138323833383438353836383738383839384038413842384338443845384638473848384938503851385238533854385538563857385838593860386138623863386438653866386738683869387038713872387338743875387638773878387938803881388238833884388538863887388838893890389138923893389438953896389738983899390039013902390339043905390639073908390939103911391239133914391539163917391839193920392139223923392439253926392739283929393039313932393339343935393639373938393939403941394239433944394539463947394839493950395139523953395439553956395739583959396039613962396339643965396639673968396939703971397239733974397539763977397839793980398139823983398439853986398739883989399039913992399339943995399639973998399940004001400240034004400540064007400840094010401140124013401440154016401740184019402040214022402340244025402640274028402940304031403240334034403540364037403840394040404140424043404440454046404740484049405040514052405340544055405640574058405940604061406240634064406540664067406840694070407140724073407440754076407740784079408040814082408340844085408640874088408940904091409240934094409540964097409840994100410141024103410441054106410741084109411041114112411341144115411641174118411941204121412241234124412541264127412841294130413141324133413441354136413741384139414041414142414341444145414641474148414941504151415241534154415541564157415841594160416141624163416441654166416741684169417041714172417341744175417641774178417941804181418241834184418541864187418841894190419141924193419441954196419741984199420042014202420342044205420642074208420942104211421242134214421542164217421842194220422142224223422442254226422742284229423042314232423342344235423642374238423942404241424242434244424542464247424842494250425142524253425442554256425742584259426042614262426342644265426642674268426942704271427242734274427542764277427842794280428142824283428442854286428742884289429042914292429342944295429642974298429943004301430243034304430543064307430843094310431143124313431443154316431743184319432043214322432343244325432643274328432943304331433243334334433543364337433843394340434143424343434443454346434743484349435043514352435343544355435643574358435943604361436243634364436543664367436843694370437143724373437443754376437743784379438043814382438343844385438643874388438943904391439243934394439543964397439843994400440144024403440444054406440744084409441044114412441344144415441644174418441944204421442244234424442544264427442844294430443144324433443444354436443744384439444044414442444344444445444644474448444944504451445244534454445544564457445844594460446144624463446444654466446744684469447044714472447344744475447644774478447944804481448244834484448544864487448844894490449144924493449444954496449744984499450045014502450345044505450645074508450945104511451245134514451545164517451845194520452145224523452445254526452745284529453045314532453345344535453645374538453945404541454245434544454545464547454845494550455145524553455445554556455745584559456045614562456345644565456645674568456945704571457245734574457545764577457845794580458145824583458445854586458745884589459045914592459345944595459645974598459946004601460246034604460546064607460846094610461146124613461446154616461746184619462046214622462346244625462646274628462946304631463246334634463546364637463846394640464146424643464446454646464746484649465046514652465346544655465646574658465946604661466246634664466546664667466846694670467146724673467446754676467746784679468046814682468346844685468646874688468946904691469246934694469546964697469846994700470147024703470447054706470747084709471047114712471347144715471647174718471947204721472247234724472547264727472847294730473147324733473447354736473747384739474047414742474347444745474647474748474947504751475247534754475547564757475847594760476147624763476447654766476747684769477047714772477347744775477647774778477947804781478247834784478547864787478847894790479147924793479447954796479747984799480048014802480348044805480648074808480948104811481248134814481548164817481848194820482148224823482448254826482748284829483048314832483348344835483648374838483948404841484248434844484548464847484848494850485148524853485448554856485748584859486048614862486348644865486648674868486948704871487248734874487548764877487848794880488148824883488448854886488748884889489048914892489348944895489648974898489949004901490249034904490549064907490849094910491149124913491449154916491749184919492049214922492349244925492649274928#!/usr/bin/env nodeprocess.noDeprecation = true;/** * 🔮 Keeps - Tezos FA2 Contract Management with Taquito * * A comprehensive Node.js module for deploying and managing * the Aesthetic Computer "Keeps" FA2 contract on Tezos. * * Usage: * node keeps.mjs deploy [network] - Deploy contract (supports --contract profile) * node keeps.mjs keep <piece> - Keep (preserve) a KidLisp piece * node keeps.mjs status - Check contract status * node keeps.mjs balance - Check wallet balance * node keeps.mjs tokens - List wallet tokens for active keeps contract * node keeps.mjs market - Show Objkt market snapshot * node keeps.mjs sell <token> <xtz> - List a token on Objkt marketplace * node keeps.mjs accept <offer_id> - Accept a specific Objkt offer * node keeps.mjs accept:auto ... - Accept best offers above thresholds * node keeps.mjs buy <ask_id> - Buy a listed token (fulfill_ask) * node keeps.mjs upload <piece> - Upload bundle to IPFS */
import { TezosToolkit, MichelsonMap } from '@taquito/taquito';import { InMemorySigner } from '@taquito/signer';import { Parser, packDataBytes } from '@taquito/michel-codec';import { MongoClient } from 'mongodb';import fs from 'fs';import path from 'path';import { fileURLToPath } from 'url';import crypto from 'crypto';import readline from 'readline';
const __dirname = path.dirname(fileURLToPath(import.meta.url));const KEEPS_SECRET_ID = process.env.KEEPS_SECRET_ID || 'tezos-kidlisp';const KEEP_PERMIT_TTL_MS = Number.parseInt(process.env.KEEP_PERMIT_TTL_MS || '1200000', 10); // 20 minutesconst OBJKT_DATA_API = 'https://data.objkt.com/v3/graphql';const OBJKT_MARKETPLACE_FALLBACK = { mainnet: 'KT1SwbTqhSKF6Pdokiu1K4Fpi17ahPPzmt1X', // objktcom marketplace v6.2};
// SmartPy-compiled contracts (v11 keeps, objkt marketplace) populate// `methodsObject` only — `methods` is empty. Older code in this file assumes// `.methods.X(...)` works; this helper picks whichever binding is present so// both styles continue to work without touching every call site.function pickMethod(contract, name) { const fn = contract?.methodsObject?.[name] || contract?.methods?.[name]; if (!fn) { throw new Error(`Contract ${contract?.address || '<unknown>'} has no entrypoint "${name}"`); } return fn.bind(contract.methodsObject?.[name] ? contract.methodsObject : contract.methods);}
// ============================================================================// Configuration// ============================================================================
const CONFIG = { // Network settings ghostnet: { rpc: 'https://rpc.ghostnet.teztnets.com', // Changed from ecadinfra name: 'Ghostnet (Testnet)', explorer: 'https://ghostnet.tzkt.io' }, mainnet: { rpc: 'https://rpc.tzbeta.net', // mainnet.api.tez.ie + ecadinfra both dead (DNS); tzbeta is Foundation-run name: 'Mainnet', explorer: 'https://tzkt.io' }, // IPFS settings pinata: { apiUrl: 'https://api.pinata.cloud', gateway: 'https://ipfs.aesthetic.computer' }, // Oven service for thumbnails oven: { url: process.env.OVEN_URL || 'https://oven.aesthetic.computer' }, // Contract paths paths: { // Compiled contract artifacts by generation compiled: { v11: path.join(__dirname, 'KeepsFA2v11/step_002_cont_0_contract.tz'), v10: path.join(__dirname, 'KeepsFA2v10/step_002_cont_0_contract.tz'), v9: path.join(__dirname, 'KeepsFA2v9/step_002_cont_0_contract.tz'), v8: path.join(__dirname, 'KeepsFA2v8/step_002_cont_0_contract.tz'), v7: path.join(__dirname, 'KeepsFA2v7/step_002_cont_0_contract.tz'), v6: path.join(__dirname, 'KeepsFA2v6/step_002_cont_0_contract.tz'), v2: path.join(__dirname, 'KeepsFA2v2/step_002_cont_0_contract.tz'), v3: path.join(__dirname, 'KeepsFA2v3/step_002_cont_0_contract.tz'), v4: path.join(__dirname, 'KeepsFA2v4/step_002_cont_0_contract.tz'), v5: path.join(__dirname, 'KeepsFA2v5/step_002_cont_0_contract.tz'), }, // Backward-compatible defaults contract: path.join(__dirname, 'KeepsFA2v9/step_002_cont_0_contract.tz'), storage: path.join(__dirname, 'KeepsFA2v9/step_002_cont_0_storage.tz'), // Legacy direct paths v3Contract: path.join(__dirname, 'KeepsFA2v3/step_002_cont_0_contract.tz'), v2Contract: path.join(__dirname, 'KeepsFA2v2/step_002_cont_0_contract.tz'), // Legacy contract path legacyContract: path.join(__dirname, 'michelson-lib/keeps-fa2-complete.tz'), // Network-specific contract addresses contractAddresses: { ghostnet: path.join(__dirname, 'contract-address-ghostnet.txt'), mainnet: path.join(__dirname, 'contract-address-mainnet.txt'), }, // Legacy single file (deprecated) contractAddress: path.join(__dirname, 'contract-address.txt'), vault: path.join(__dirname, '../aesthetic-computer-vault') }};
const CONTRACT_PROFILES = { v11: { key: 'v11', label: 'KidLisp v11 — user-only minting, no admin path', artifactKey: 'v11', metadata: { name: 'KidLisp', version: '11.0.0', description: 'https://keep.kidlisp.com', homepage: 'https://keep.kidlisp.com', interfaces: ['TZIP-012', 'TZIP-016', 'TZIP-021'], authors: ['aesthetic.computer'], imageUri: 'https://oven.aesthetic.computer/keeps/latest', }, keepFeeMutez: 2_500_000, artistRoyaltyBps: 900, platformRoyaltyBps: 100, paused: false, }, v10: { key: 'v10', label: 'KidLisp v10 — no admin_transfer, split royalties', artifactKey: 'v10', metadata: { name: 'KidLisp', version: '10.0.0', description: 'https://keep.kidlisp.com', homepage: 'https://keep.kidlisp.com', interfaces: ['TZIP-012', 'TZIP-016', 'TZIP-021'], authors: ['aesthetic.computer'], imageUri: 'https://oven.aesthetic.computer/keeps/latest', }, keepFeeMutez: 2_500_000, artistRoyaltyBps: 900, platformRoyaltyBps: 100, paused: false, }, v9: { key: 'v9', label: 'KidLisp v9 final production', artifactKey: 'v9', metadata: { name: 'KidLisp', version: '9.0.0', description: 'https://keep.kidlisp.com', homepage: 'https://keep.kidlisp.com', interfaces: ['TZIP-012', 'TZIP-016', 'TZIP-021'], authors: ['aesthetic.computer'], imageUri: 'https://oven.aesthetic.computer/keeps/latest', }, keepFeeMutez: 2_500_000, defaultRoyaltyBps: 1000, paused: false, }, v8: { key: 'v8', label: 'KidLisp v8 signed-permit production', artifactKey: 'v8', metadata: { name: 'KidLisp', version: '8.0.0', description: 'https://keep.kidlisp.com', homepage: 'https://keep.kidlisp.com', interfaces: ['TZIP-012', 'TZIP-016', 'TZIP-021'], authors: ['aesthetic.computer'], imageUri: 'https://oven.aesthetic.computer/keeps/latest', }, keepFeeMutez: 2_500_000, defaultRoyaltyBps: 1000, paused: false, }, v7: { key: 'v7', label: 'KidLisp v7 final production', artifactKey: 'v7', metadata: { name: 'KidLisp', version: '7.0.0', description: 'https://keep.kidlisp.com', homepage: 'https://kidlisp.com', interfaces: ['TZIP-012', 'TZIP-016', 'TZIP-021'], authors: ['aesthetic.computer'], imageUri: 'https://oven.aesthetic.computer/keeps/latest', }, keepFeeMutez: 2_500_000, defaultRoyaltyBps: 1000, paused: false, }, v6: { key: 'v6', label: 'KidLisp v6 production (legacy)', artifactKey: 'v6', metadata: { name: 'KidLisp', version: '6.0.0', description: 'https://keep.kidlisp.com', homepage: 'https://kidlisp.com', interfaces: ['TZIP-012', 'TZIP-016', 'TZIP-021'], authors: ['aesthetic.computer'], imageUri: 'https://oven.aesthetic.computer/keeps/latest', }, keepFeeMutez: 2_500_000, defaultRoyaltyBps: 1000, paused: false, }, v5rc: { key: 'v5rc', label: 'KidLisp v5 release candidate', artifactKey: 'v5', metadata: { name: 'KidLisp Keeps RC', version: '5.0.0-rc', description: 'https://keep.kidlisp.com/rc', homepage: 'https://kidlisp.com', interfaces: ['TZIP-012', 'TZIP-016', 'TZIP-021'], authors: ['aesthetic.computer'], imageUri: 'https://oven.aesthetic.computer/keeps/latest', }, keepFeeMutez: 2_500_000, defaultRoyaltyBps: 1000, paused: false, }, v4: { key: 'v4', label: 'Keeps v4 legacy', artifactKey: 'v4', metadata: { name: 'KidLisp Keeps Beta', version: '4.0.0', interfaces: ['TZIP-012', 'TZIP-016', 'TZIP-021'], authors: ['aesthetic.computer'], homepage: 'https://aesthetic.computer', imageUri: 'https://oven.aesthetic.computer/keeps/latest', }, keepFeeMutez: 0, defaultRoyaltyBps: 1000, paused: false, },};
function resolveContractProfile(rawProfile = 'v9') { const normalized = String(rawProfile || 'v9').trim().toLowerCase(); const aliasMap = { rc: 'v5rc', v5: 'v5rc', production: 'v11', latest: 'v11', }; const key = aliasMap[normalized] || normalized; const profile = CONTRACT_PROFILES[key]; if (!profile) { const supported = Object.keys(CONTRACT_PROFILES).join(', '); throw new Error(`❌ Unknown contract profile: ${rawProfile}. Use one of: ${supported}`); } return profile;}
function getMongoSecretsConfig() { return { connectionString: process.env.MONGODB_CONNECTION_STRING, dbName: process.env.MONGODB_NAME, };}
async function syncActiveKeepsSecret({ network = 'mainnet', contractAddress, profile }) { const { connectionString, dbName } = getMongoSecretsConfig(); if (!connectionString || !dbName) { console.log(' ⚠️ Mongo secrets sync skipped (set MONGODB_CONNECTION_STRING + MONGODB_NAME to enable).'); return { synced: false, reason: 'missing-mongo-env' }; }
const client = new MongoClient(connectionString, { serverSelectionTimeoutMS: 10000, connectTimeoutMS: 10000, });
try { await client.connect(); const secrets = client.db(dbName).collection('secrets'); const now = new Date().toISOString();
const update = await secrets.updateOne( { _id: KEEPS_SECRET_ID }, { $set: { [`keepsContract.${network}`]: contractAddress, currentKeepsContract: contractAddress, currentKeepsNetwork: network, currentKeepsProfile: profile.key, currentKeepsVersion: profile.metadata?.version || null, currentKeepsUpdatedAt: now, }, } );
if (!update.matchedCount) { console.log(` ⚠️ Mongo secrets sync skipped (no secrets.${KEEPS_SECRET_ID} document found).`); return { synced: false, reason: 'secret-not-found' }; }
console.log(` 🗄️ Synced secrets.${KEEPS_SECRET_ID} -> ${contractAddress} (${profile.key} on ${network})`); return { synced: true }; } catch (error) { console.log(` ⚠️ Mongo secrets sync failed: ${error.message}`); return { synced: false, reason: 'sync-error', error: error.message }; } finally { await client.close().catch(() => {}); }}
async function syncCurrentContractToSecrets(network = 'mainnet', options = {}) { const profile = resolveContractProfile(options.contractProfile || options.profile || 'v9'); const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No saved contract address for ${network} at ${addressPath}`); }
const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); if (!isKt1Address(contractAddress)) { throw new Error(`❌ Invalid contract address in ${addressPath}: ${contractAddress}`); }
console.log(`\n🗄️ Syncing secrets from ${addressPath}`); console.log(` Contract: ${contractAddress}`); console.log(` Profile: ${profile.key}`); console.log(` Network: ${network}`);
return syncActiveKeepsSecret({ network, contractAddress, profile });}
// ============================================================================// Credential Loading// ============================================================================
// Current wallet selection (can be changed via --wallet flag)let currentWallet = 'staging'; // default for mainnet staging contract
function setWallet(wallet) { currentWallet = wallet;}
function loadCredentials() { const credentials = {}; // Load Tezos wallet credentials based on current wallet selection // Note: aesthetic wallet keys are stored in kidlisp/.env for convenience const walletPaths = { keeps: { path: 'tezos/kidlisp/.env', addressKey: 'KEEPS_ADDRESS', secretKey: 'KEEPS_KEY' }, kidlisp: { path: 'tezos/kidlisp/.env', addressKey: 'KIDLISP_ADDRESS', secretKey: 'KIDLISP_KEY' }, aesthetic: { path: 'tezos/kidlisp/.env', addressKey: 'AESTHETIC_ADDRESS', secretKey: 'AESTHETIC_KEY' }, staging: { path: 'tezos/staging/.env', addressKey: 'STAGING_ADDRESS', secretKey: 'STAGING_KEY' } }; const walletConfig = walletPaths[currentWallet] || walletPaths.kidlisp; const tezosEnvPath = path.join(CONFIG.paths.vault, walletConfig.path); if (fs.existsSync(tezosEnvPath)) { const content = fs.readFileSync(tezosEnvPath, 'utf8'); for (const line of content.split('\n')) { // Try both specific keys and generic ADDRESS/KEY patterns if (line.startsWith(walletConfig.addressKey + '=') || line.startsWith('ADDRESS=')) { credentials.address = line.split('=')[1].trim().replace(/"/g, ''); } else if (line.startsWith(walletConfig.secretKey + '=') || line.startsWith('KEY=') || line.startsWith('SECRET_KEY=')) { credentials.secretKey = line.split('=')[1].trim().replace(/"/g, ''); } } } // Load Pinata credentials const pinataEnvPath = path.join(CONFIG.paths.vault, '.env.pinata'); if (fs.existsSync(pinataEnvPath)) { const content = fs.readFileSync(pinataEnvPath, 'utf8'); for (const line of content.split('\n')) { if (line.startsWith('PINATA_API_KEY=')) { credentials.pinataKey = line.split('=')[1].trim().replace(/"/g, ''); } else if (line.startsWith('PINATA_API_SECRET=')) { credentials.pinataSecret = line.split('=')[1].trim().replace(/"/g, ''); } } } credentials.wallet = currentWallet; return credentials;}
// Get contract address file path for a networkfunction getContractAddressPath(network = 'mainnet') { // Use network-specific paths if available if (CONFIG.paths.contractAddresses[network]) { return CONFIG.paths.contractAddresses[network]; } // Fall back to legacy single file return CONFIG.paths.contractAddress;}
function isKt1Address(value) { return typeof value === 'string' && /^KT1[1-9A-HJ-NP-Za-km-z]{33}$/.test(value.trim());}
function formatExtendedError(error) { if (!error || typeof error !== 'object') return '';
const payload = error.body ?? error.errors ?? error.data ?? null; if (!payload) return '';
try { return JSON.stringify(payload, null, 2); } catch { return String(payload); }}
function tzktApiBase(network = 'mainnet') { return network === 'mainnet' ? 'https://api.tzkt.io' : `https://api.${network}.tzkt.io`;}
async function objktGraphQL(query, variables = {}) { const response = await fetch(OBJKT_DATA_API, { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ query, variables }), });
if (!response.ok) { throw new Error(`Objkt data API returned ${response.status}`); }
const payload = await response.json(); if (Array.isArray(payload.errors) && payload.errors.length > 0) { throw new Error(`Objkt data API error: ${payload.errors[0].message || 'Unknown error'}`); }
return payload.data || {};}
function parseVersionFromLabel(label = '') { const match = String(label).match(/v(\d+(?:\.\d+)*)/i); if (!match) return []; return match[1].split('.').map((part) => Number.parseInt(part, 10)).filter(Number.isFinite);}
function compareVersionArrays(a = [], b = []) { const maxLength = Math.max(a.length, b.length); for (let i = 0; i < maxLength; i += 1) { const av = a[i] ?? 0; const bv = b[i] ?? 0; if (av !== bv) return av - bv; } return 0;}
async function resolveObjktMarketplaceContract({ network = 'mainnet', keepsContract, explicitContract = null,}) { if (explicitContract) return explicitContract;
if (network !== 'mainnet') { throw new Error( `Objkt marketplace auto-discovery only supports mainnet. Pass --marketplace=<KT1...> for ${network}.` ); }
try { const existingData = await objktGraphQL( ` query($contract:String!) { listing_active( where:{fa_contract:{_eq:$contract}} order_by:{timestamp:desc} limit:1 ) { marketplace_contract marketplace { name } } } `, { contract: keepsContract } );
const existing = existingData?.listing_active?.[0]; if (isKt1Address(existing?.marketplace_contract)) { return existing.marketplace_contract; } } catch { // Fallback to registry lookup below. }
const registryData = await objktGraphQL(` query { marketplace_contract( where:{ group:{_eq:"objktcom"}, subgroup:{_eq:"marketplace"}, name:{_ilike:"objktcom marketplace v%"} } ) { contract name } } `);
const rows = Array.isArray(registryData?.marketplace_contract) ? registryData.marketplace_contract : [];
const ranked = rows .filter((row) => isKt1Address(row?.contract)) .map((row) => ({ ...row, parsedVersion: parseVersionFromLabel(row?.name), })) .sort((a, b) => compareVersionArrays(b.parsedVersion, a.parsedVersion));
if (ranked.length > 0) { return ranked[0].contract; }
const fallback = OBJKT_MARKETPLACE_FALLBACK[network]; if (isKt1Address(fallback)) return fallback;
throw new Error('Could not resolve Objkt marketplace contract.');}
function loadContractAddress(network = 'mainnet') { const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No contract deployed on ${network}. Run: node keeps.mjs deploy ${network}`); } return fs.readFileSync(addressPath, 'utf8').trim();}
function parsePriceToMutez(priceInput) { const asNumber = Number.parseFloat(String(priceInput)); if (!Number.isFinite(asNumber) || asNumber <= 0) { throw new Error(`Invalid price "${priceInput}". Expected a positive XTZ value.`); } return Math.round(asNumber * 1_000_000);}
function parseOptionalIsoTimestamp(value, label) { if (value == null || value === '') return null; const date = new Date(value); if (Number.isNaN(date.getTime())) { throw new Error(`Invalid ${label} timestamp "${value}". Use ISO-8601 format.`); } return date.toISOString();}
async function resolveTokenIdFromReference(tokenReference, { contractAddress, network = 'mainnet' }) { const raw = String(tokenReference || '').trim(); if (!raw) { throw new Error('Token reference is required (token id or piece code like $bip).'); }
if (/^\d+$/.test(raw)) { return Number.parseInt(raw, 10); }
const pieceName = raw.replace(/^\$/, ''); const duplicate = await checkDuplicatePiece(pieceName, contractAddress, network); if (duplicate.exists) { return Number.parseInt(duplicate.tokenId, 10); }
throw new Error(`Could not resolve token reference "${tokenReference}" to a token id.`);}
function normalizeShareMap(shares = {}) { const normalized = {}; for (const [address, amount] of Object.entries(shares || {})) { const nat = Number.parseInt(String(amount), 10); if (nat > 0) { normalized[address] = nat.toString(); } } return normalized;}
async function fetchTokenFromTzkt(contractAddress, tokenId, network = 'mainnet') { const apiBase = tzktApiBase(network); const url = `${apiBase}/v1/tokens?contract=${contractAddress}&tokenId=${tokenId}`; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch token #${tokenId} from TzKT (${response.status})`); } const rows = await response.json(); return rows?.[0] || null;}
async function assertWalletOwnsToken(contractAddress, tokenId, ownerAddress, network = 'mainnet') { const apiBase = tzktApiBase(network); const url = `${apiBase}/v1/tokens/balances?token.contract=${contractAddress}&token.tokenId=${tokenId}&account=${ownerAddress}&balance.gt=0&limit=1`; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to verify ownership for token #${tokenId} (${response.status})`); } const rows = await response.json(); if (!Array.isArray(rows) || rows.length === 0) { throw new Error(`Wallet ${ownerAddress} does not currently hold token #${tokenId}.`); }}
async function loadActiveListingForToken(contractAddress, tokenId, sellerAddress) { const data = await objktGraphQL( ` query($contract:String!, $tokenId:String!, $seller:String!) { listing_active( where:{ fa_contract:{_eq:$contract}, seller_address:{_eq:$seller}, token:{token_id:{_eq:$tokenId}} } order_by:{timestamp:desc} limit:1 ) { id bigmap_key price_xtz marketplace_contract marketplace { name } } } `, { contract: contractAddress, tokenId: String(tokenId), seller: sellerAddress } ); return data?.listing_active?.[0] || null;}
async function loadBestActiveOfferForToken(contractAddress, tokenId) { const data = await objktGraphQL( ` query($contract:String!, $tokenId:String!) { offer_active( where:{ fa_contract:{_eq:$contract}, token:{token_id:{_eq:$tokenId}} } order_by:{price_xtz:desc} limit:1 ) { id bigmap_key price_xtz buyer_address marketplace_contract amount_left timestamp token { token_id name fa_contract } } } `, { contract: contractAddress, tokenId: String(tokenId) } ); return data?.offer_active?.[0] || null;}
async function loadActiveOfferById(offerId) { const numericId = Number.parseInt(String(offerId), 10); if (!Number.isInteger(numericId) || numericId < 0) { throw new Error(`Invalid offer id "${offerId}".`); }
const data = await objktGraphQL( ` query { offer_active( where:{ _or:[ {id:{_eq:${numericId}}}, {bigmap_key:{_eq:${numericId}}} ] } order_by:{timestamp:desc} limit:5 ) { id bigmap_key price_xtz buyer_address marketplace_contract amount_left timestamp token { token_id name fa_contract } } } ` );
const rows = Array.isArray(data?.offer_active) ? data.offer_active : []; if (rows.length === 0) return null;
// Prefer direct row-id match first, then on-chain offer-id (bigmap key). return rows.find((row) => Number.parseInt(String(row?.id), 10) === numericId) || rows.find((row) => Number.parseInt(String(row?.bigmap_key), 10) === numericId) || rows[0];}
// ============================================================================// Tezos Client Setup// ============================================================================
async function createTezosClient(network = 'mainnet') { const credentials = loadCredentials(); if (!credentials.address || !credentials.secretKey) { throw new Error('❌ Tezos credentials not found in vault'); } const config = CONFIG[network]; const tezos = new TezosToolkit(config.rpc); // Set up signer tezos.setProvider({ signer: new InMemorySigner(credentials.secretKey) }); return { tezos, credentials, config };}
// ============================================================================// Contract Deployment// ============================================================================
async function deployContract(network = 'mainnet', options = {}) { const profile = resolveContractProfile(options.contractProfile || options.profile || 'v9'); const contractPath = CONFIG.paths.compiled[profile.artifactKey] || CONFIG.paths.contract; const feeXTZ = (profile.keepFeeMutez / 1_000_000).toFixed(6); const pausedMichelson = profile.paused ? 'True' : 'False';
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log(`║ 🚀 Deploying ${profile.label.padEnd(48)}║`); console.log('╚══════════════════════════════════════════════════════════════╝\n');
const { tezos, credentials, config } = await createTezosClient(network);
console.log(`📡 Network: ${config.name}`); console.log(`📍 RPC: ${config.rpc}`); console.log(`👤 Administrator: ${credentials.address}`); console.log(`🧱 Profile: ${profile.key}\n`);
// Check balance console.log('💰 Checking balance...'); const balance = await tezos.tz.getBalance(credentials.address); const balanceXTZ = balance.toNumber() / 1_000_000; console.log(` Balance: ${balanceXTZ.toFixed(2)} XTZ`);
if (balanceXTZ < 1) { throw new Error('❌ Insufficient balance. Need at least 1 XTZ for deployment.'); }
// Load contract code (SmartPy compiled Michelson) console.log('\n📄 Loading contract...'); if (!fs.existsSync(contractPath)) { throw new Error(`❌ Contract file not found: ${contractPath}\n Compile the selected artifact before deploy.`); }
const contractSource = fs.readFileSync(contractPath, 'utf8'); console.log(` ✓ Contract loaded: ${path.relative(__dirname, contractPath)}`);
// Parse the contract using michel-codec const parser = new Parser(); const parsedContract = parser.parseScript(contractSource);
console.log('\n💾 Creating initial storage...');
const contractMetadataJson = JSON.stringify(profile.metadata); const contractMetadataBytes = stringToBytes(contractMetadataJson); const tezosStoragePointer = stringToBytes('tezos-storage:content');
// Storage layout varies by version (SmartPy sorts fields alphabetically). // v10 adds: artist_royalty_bps, platform_royalty_bps, treasury_address // removes: default_royalty_bps // v10 order: administrator, artist_royalty_bps, content_hashes, // contract_metadata_locked, keep_fee, ledger, metadata, // metadata_locked, next_token_id, operators, paused, // platform_royalty_bps, token_creators, token_metadata, treasury_address const isV10 = profile.key === 'v10' || profile.key === 'v11'; const treasuryAddress = credentials.treasuryAddress || credentials.address;
let initialStorageMichelson; if (isV10) { initialStorageMichelson = `(Pair "${credentials.address}" (Pair ${profile.artistRoyaltyBps} (Pair {} (Pair False (Pair ${profile.keepFeeMutez} (Pair {} (Pair {Elt "" 0x${tezosStoragePointer}; Elt "content" 0x${contractMetadataBytes}} (Pair {} (Pair 0 (Pair {} (Pair ${pausedMichelson} (Pair ${profile.platformRoyaltyBps} (Pair {} (Pair {} "${treasuryAddress}")))))))))))))))`; } else { // v9 and earlier: administrator, content_hashes, contract_metadata_locked, // default_royalty_bps, keep_fee, ledger, metadata, metadata_locked, // next_token_id, operators, paused, token_creators, token_metadata initialStorageMichelson = `(Pair "${credentials.address}" (Pair {} (Pair False (Pair ${profile.defaultRoyaltyBps} (Pair ${profile.keepFeeMutez} (Pair {} (Pair {Elt "" 0x${tezosStoragePointer}; Elt "content" 0x${contractMetadataBytes}} (Pair {} (Pair 0 (Pair {} (Pair ${pausedMichelson} (Pair {} {}))))))))))))`; } const parsedStorage = parser.parseMichelineExpression(initialStorageMichelson);
console.log(` ✓ Name: ${profile.metadata.name}`); console.log(` ✓ Version: ${profile.metadata.version}`); if (profile.metadata.description) { console.log(` ✓ Description: ${profile.metadata.description}`); } console.log(` ✓ Homepage: ${profile.metadata.homepage}`); console.log(` ✓ Initial token ID: 0`); console.log(` ✓ Keep fee: ${profile.keepFeeMutez} mutez (${feeXTZ} XTZ)`); if (profile.artistRoyaltyBps !== undefined) { console.log(` ✓ Artist royalty: ${profile.artistRoyaltyBps} bps / Platform: ${profile.platformRoyaltyBps} bps`); console.log(` ✓ Treasury: ${treasuryAddress}`); } else { console.log(` ✓ Default royalty: ${profile.defaultRoyaltyBps} bps`); } console.log(` ✓ Paused: ${profile.paused}`);
console.log('\n📤 Deploying contract...'); console.log(' (This may take 1-2 minutes...)\n');
try { const originationOp = await tezos.contract.originate({ code: parsedContract, init: parsedStorage });
console.log(` ⏳ Operation hash: ${originationOp.hash}`); console.log(' ⏳ Waiting for confirmation...');
await originationOp.confirmation(1);
const contractAddress = originationOp.contractAddress;
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ ✅ Contract Deployed Successfully! ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
console.log(` 📍 Contract Address: ${contractAddress}`); console.log(` 🔗 Explorer: ${config.explorer}/${contractAddress}`); console.log(` 🖼️ Objkt: https://${network === 'mainnet' ? '' : 'ghostnet.'}objkt.com/collection/${contractAddress}`); console.log(` 📝 Operation: ${config.explorer}/${originationOp.hash}\n`);
// Save contract address (network-specific file) const addressPath = getContractAddressPath(network); fs.writeFileSync(addressPath, contractAddress); console.log(` 💾 Saved address to: ${addressPath}\n`);
await syncActiveKeepsSecret({ network, contractAddress, profile }); console.log('');
return { address: contractAddress, hash: originationOp.hash, profile: profile.key };
} catch (error) { console.error('\n❌ Deployment failed!'); console.error(` Error: ${error.message}`); if (error.message.includes('bad_stack')) { console.error('\n 💡 This usually means storage format mismatch.'); console.error(' Check that the storage matches the selected contract artifact.'); } throw error; }}
// ============================================================================// Contract Status// ============================================================================
async function getContractStatus(network = 'mainnet') { const { tezos, config } = await createTezosClient(network); // Load contract address (network-specific) const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No contract deployed on ${network}. Run: node keeps.mjs deploy ${network}`); } const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 📊 Contract Status ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${contractAddress}`); console.log(`🔗 Explorer: ${config.explorer}/${contractAddress}\n`); try { const contract = await tezos.contract.at(contractAddress); const storage = await contract.storage(); console.log('📦 Storage:'); console.log(` Administrator: ${storage.administrator}`); console.log(` Next Token ID: ${storage.next_token_id.toString()}`); console.log(` Total Keeps: ${storage.next_token_id.toString()}`); const totalTokens = storage.next_token_id.toNumber(); // For large collections, use TzKT API with pagination // Only show recent tokens to avoid O(n) RPC calls const MAX_DISPLAY = 10; if (totalTokens > 0) { console.log(`\n🎨 Recent Tokens (showing last ${Math.min(MAX_DISPLAY, totalTokens)} of ${totalTokens}):`); // Fetch recent tokens via TzKT (efficient, paginated) const tzktUrl = `https://api.${network}.tzkt.io/v1/contracts/${contractAddress}/bigmaps/token_metadata/keys?limit=${MAX_DISPLAY}&sort.desc=id`; const objktBase = network === 'mainnet' ? 'https://objkt.com' : 'https://ghostnet.objkt.com'; try { const response = await fetch(tzktUrl); if (response.ok) { const tokens = await response.json(); for (const token of tokens.reverse()) { const tokenId = token.key; const tokenInfo = token.value?.token_info || {}; const name = tokenInfo.name ? Buffer.from(tokenInfo.name, 'hex').toString() : `#${tokenId}`; // Check if metadata is locked (bigmap entry must exist AND be true) const lockValue = await storage.metadata_locked?.get?.(parseInt(tokenId)); const locked = lockValue === true ? ' 🔒' : ''; const objktUrl = `${objktBase}/asset/${contractAddress}/${tokenId}`; console.log(` [${tokenId}] ${name}${locked}`); console.log(` 🔗 ${objktUrl}`); } } else { console.log(' (Use TzKT explorer to view all tokens)'); } } catch (e) { console.log(' (Could not fetch token list from TzKT)'); } if (totalTokens > MAX_DISPLAY) { console.log(` ... and ${totalTokens - MAX_DISPLAY} more`); } } return { address: contractAddress, storage }; } catch (error) { console.error(`\n❌ Failed to get contract status: ${error.message}`); throw error; }}
// ============================================================================// Multi-Chain Wallet Balances// ============================================================================
async function getAllWalletBalances() { const walletsPath = path.join(CONFIG.paths.vault, 'wallets/wallets.json'); if (!fs.existsSync(walletsPath)) { console.error('❌ wallets.json not found in vault'); process.exit(1); } const { wallets } = JSON.parse(fs.readFileSync(walletsPath, 'utf8'));
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🌐 All Wallet Balances ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
// Group wallets by chain const chains = {}; for (const [key, w] of Object.entries(wallets)) { (chains[w.chain] ||= []).push({ key, ...w }); }
// --- Tezos --- if (chains.tezos) { console.log('── Tezos ──────────────────────────────────────────────────────'); const tezos = new TezosToolkit(CONFIG.mainnet.rpc); for (const w of chains.tezos) { try { const bal = await tezos.tz.getBalance(w.address); const xtz = (bal.toNumber() / 1_000_000).toFixed(6); const label = w.domain || w.name; console.log(` ${label.padEnd(22)} ${xtz.padStart(14)} XTZ ${w.address}`); } catch (e) { console.log(` ${(w.domain || w.name).padEnd(22)} ${'error'.padStart(14)} ${w.address} (${e.message})`); } } // Also show keeps contract balance try { const contractAddr = fs.readFileSync( CONFIG.paths.contractAddresses.mainnet, 'utf8' ).trim(); const bal = await tezos.tz.getBalance(contractAddr); const xtz = (bal.toNumber() / 1_000_000).toFixed(6); console.log(` ${'keeps contract'.padEnd(22)} ${xtz.padStart(14)} XTZ ${contractAddr}`); } catch (e) { /* skip */ } console.log(); }
// --- Ethereum --- if (chains.ethereum) { console.log('── Ethereum ───────────────────────────────────────────────────'); for (const w of chains.ethereum) { try { const resp = await fetch('https://ethereum-rpc.publicnode.com', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', method: 'eth_getBalance', params: [w.address, 'latest'], id: 1 }), }); const data = await resp.json(); const eth = (Number(BigInt(data.result)) / 1e18).toFixed(6); const label = w.domain || w.name; console.log(` ${label.padEnd(22)} ${eth.padStart(14)} ETH ${w.address}`); } catch (e) { console.log(` ${(w.domain || w.name).padEnd(22)} ${'error'.padStart(14)} ${w.address}`); } } console.log(); }
// --- Solana --- if (chains.solana) { console.log('── Solana ─────────────────────────────────────────────────────'); for (const w of chains.solana) { try { const resp = await fetch('https://solana-rpc.publicnode.com', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'getBalance', params: [w.address] }), }); const data = await resp.json(); const sol = (data.result.value / 1e9).toFixed(6); const label = w.name; console.log(` ${label.padEnd(22)} ${sol.padStart(14)} SOL ${w.address}`); } catch (e) { console.log(` ${(w.domain || w.name).padEnd(22)} ${'error'.padStart(14)} ${w.address}`); } } console.log(); }
// --- Bitcoin --- if (chains.bitcoin) { console.log('── Bitcoin ────────────────────────────────────────────────────'); for (const w of chains.bitcoin) { // Check both taproot and segwit addresses const addr = w.address_taproot || w.address_segwit || w.address; if (!addr) { console.log(` ${w.name.padEnd(22)} ${'no address'.padStart(14)} (needs derivation)`); continue; } try { const resp = await fetch(`https://blockstream.info/api/address/${addr}`); const data = await resp.json(); // funded = total received, spent = total sent; chain_stats for confirmed const confirmed = data.chain_stats || {}; const satoshis = (confirmed.funded_txo_sum || 0) - (confirmed.spent_txo_sum || 0); const btc = (satoshis / 1e8).toFixed(8); const label = w.name; console.log(` ${label.padEnd(22)} ${btc.padStart(14)} BTC ${addr}`); } catch (e) { console.log(` ${w.name.padEnd(22)} ${'error'.padStart(14)} ${addr}`); } } console.log(); }
// --- Cardano --- if (chains.cardano) { console.log('── Cardano ────────────────────────────────────────────────────'); for (const w of chains.cardano) { if (!w.address) { console.log(` ${w.name.padEnd(22)} ${'no address'.padStart(14)} (derive from mnemonic with cardano-serialization-lib)`); continue; } try { const resp = await fetch('https://api.koios.rest/api/v1/address_info', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ _addresses: [w.address] }), }); const data = await resp.json(); const lovelace = data[0]?.balance || '0'; const ada = (Number(lovelace) / 1e6).toFixed(6); console.log(` ${w.name.padEnd(22)} ${ada.padStart(14)} ADA ${w.address}`); } catch (e) { console.log(` ${w.name.padEnd(22)} ${'error'.padStart(14)} ${w.address}`); } } console.log(); }
console.log('🔗 Explorers: tzkt.io | etherscan.io | solscan.io | blockstream.info | cardanoscan.io\n');}
// ============================================================================// Wallet Balance// ============================================================================
async function getBalance(network = 'mainnet') { const { tezos, credentials, config } = await createTezosClient(network); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 💰 Wallet Balance ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${config.name}`); console.log(`👤 Address: ${credentials.address}\n`); const balance = await tezos.tz.getBalance(credentials.address); const balanceXTZ = balance.toNumber() / 1_000_000; console.log(`💵 Balance: ${balanceXTZ.toFixed(6)} XTZ`); console.log(`🔗 Explorer: ${config.explorer}/${credentials.address}\n`); return { address: credentials.address, balance: balanceXTZ };}
// ============================================================================// Content Type Detection// ============================================================================
/** * Detect if a piece is kidlisp or JavaScript mjs * KidLisp pieces start with $ and are stored via the store-kidlisp API * JavaScript pieces exist as .mjs files in disks/ */async function detectContentType(piece) { const pieceName = piece.replace(/^\$/, ''); // Check if it's a kidlisp piece (codes like 39j, abc, etc.) // KidLisp codes are 2-4 alphanumeric characters if (/^[a-z0-9]{2,4}$/i.test(pieceName)) { // Try to fetch from kidlisp API to confirm try { const response = await fetch(`https://aesthetic.computer/api/store-kidlisp?code=${pieceName}`); const data = await response.json(); if (data.source && !data.error) { return { type: 'kidlisp', source: data.source }; } } catch (e) { // Fall through to check mjs } } // Check if it exists as an mjs piece try { const response = await fetch(`https://aesthetic.computer/aesthetic.computer/disks/${pieceName}.mjs`, { method: 'HEAD' }); if (response.ok) { return { type: 'mjs', source: null }; } } catch (e) { // Continue } // Default to assuming it's a kidlisp code return { type: 'kidlisp', source: null };}
// ============================================================================// Bundle Generation via Netlify Endpoint// ============================================================================
/** * Fetch a proper self-contained HTML bundle from the Netlify endpoint * This bundles all JS, CSS, and assets into a single file that works offline */// Check if a piece name already exists in the contractasync function checkDuplicatePiece(pieceName, contractAddress, network = 'mainnet') { if (!contractAddress) { // Load contract address (network-specific) if not provided const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { return { exists: false }; // No contract deployed yet } contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); } // Query the content_hashes big_map via TzKT // Key is the piece name as hex bytes const keyBytes = stringToBytes(pieceName); // Use network-appropriate API endpoint const apiBase = network === 'mainnet' ? 'https://api.tzkt.io' : `https://api.${network}.tzkt.io`; const url = `${apiBase}/v1/contracts/${contractAddress}/bigmaps/content_hashes/keys/${keyBytes}`; try { const response = await fetch(url); if (response.status === 200) { const data = await response.json(); // Check if the key is still active (not burned) if (data.active) { return { exists: true, tokenId: data.value }; } } return { exists: false }; } catch (error) { // If query fails, assume not duplicate (will fail at contract level if it is) return { exists: false }; }}
async function fetchBundleFromNetlify(piece, contentType) { const pieceName = piece.replace(/^\$/, ''); console.log(`📦 Fetching bundle from Netlify (${contentType})...`); // Use local dev server if --local flag is set or LOCAL_BUNDLE env var const useLocal = process.env.LOCAL_BUNDLE === '1' || process.argv.includes('--local'); const baseUrl = useLocal ? 'https://localhost:8888/api/bundle-html' : 'https://aesthetic.computer/api/bundle-html'; // Build the correct endpoint URL based on content type let url; if (contentType === 'kidlisp') { url = `${baseUrl}?code=${pieceName}&format=json`; } else { url = `${baseUrl}?piece=${pieceName}&format=json`; } console.log(` URL: ${url}${useLocal ? ' (local)' : ''}`); let response; if (useLocal) { // For local dev server with self-signed cert, use https module directly const https = await import('https'); response = await new Promise((resolve, reject) => { const req = https.get(url, { rejectUnauthorized: false }, (res) => { let data = ''; res.on('data', chunk => data += chunk); res.on('end', () => { resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, json: () => Promise.resolve(JSON.parse(data)), text: () => Promise.resolve(data) }); }); }); req.on('error', reject); }); } else { response = await fetch(url); } if (!response.ok) { const errorText = await response.text(); throw new Error(`Bundle generation failed: ${errorText}`); } const data = await response.json(); if (data.error) { throw new Error(`Bundle error: ${data.error}`); } // Decode base64 content const html = Buffer.from(data.content, 'base64').toString('utf8'); console.log(` ✓ Bundle received: ${data.sizeKB} KB`); console.log(` ✓ Filename: ${data.filename}`); if (data.sourceCode) { console.log(` ✓ Source lines: ${data.sourceCode.split('\n').length}`); } if (data.authorHandle) { console.log(` ✓ Author: ${data.authorHandle}`); } if (data.userCode) { console.log(` ✓ User: ${data.userCode}`); } if (data.depCount > 0) { console.log(` ✓ Dependencies: ${data.depCount}`); } return { html, filename: data.filename, sizeKB: data.sizeKB, sourceCode: data.sourceCode, authorHandle: data.authorHandle, userCode: data.userCode, packDate: data.packDate, depCount: data.depCount, };}
// ============================================================================// IPFS Upload// ============================================================================
/** * Upload a JSON object to IPFS via Pinata */async function uploadJsonToIPFS(jsonData, name, credentials) { const jsonString = JSON.stringify(jsonData, null, 2); const blob = new Blob([jsonString], { type: 'application/json' }); const formData = new FormData(); formData.append('file', blob, 'metadata.json'); formData.append('pinataMetadata', JSON.stringify({ name })); const response = await fetch(`${CONFIG.pinata.apiUrl}/pinning/pinFileToIPFS`, { method: 'POST', headers: { 'pinata_api_key': credentials.pinataKey, 'pinata_secret_api_key': credentials.pinataSecret }, body: formData }); if (!response.ok) { const error = await response.text(); throw new Error(`Pinata JSON upload failed: ${error}`); } const result = await response.json(); return `ipfs://${result.IpfsHash}`;}
/** * Generate and upload thumbnail to IPFS via oven's grab-ipfs endpoint */async function generateThumbnail(piece, credentials, options = {}) { const { format = 'webp', width = 96, // Small thumbnail (was 512) height = 96, duration = 8000, // 8 seconds fps = 10, // 10fps capture playbackFps = 20, // 20fps playback = 2x speed density = 2, // 2x density for crisp pixels quality = 70, // Lower quality for smaller files keepId = null, // Tezos keep token ID for tracking } = options; console.log('\n📸 Generating thumbnail...'); console.log(` Oven: ${CONFIG.oven.url}`); // For local dev with self-signed certs, we need to disable cert verification const fetchOptions = { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ piece, format, width, height, duration, fps, playbackFps, density, quality, pinataKey: credentials.pinataKey, pinataSecret: credentials.pinataSecret, source: 'keep', keepId, }), }; // Disable SSL verification for localhost (self-signed certs) if (CONFIG.oven.url.includes('localhost')) { const https = await import('https'); fetchOptions.agent = new https.Agent({ rejectUnauthorized: false }); } const response = await fetch(`${CONFIG.oven.url}/grab-ipfs`, fetchOptions); if (!response.ok) { const error = await response.text(); throw new Error(`Thumbnail generation failed: ${error}`); } const result = await response.json(); if (!result.success) { throw new Error(`Thumbnail generation failed: ${result.error}`); } console.log(` ✅ Thumbnail uploaded: ${result.ipfsUri}`); console.log(` Size: ${(result.size / 1024).toFixed(2)} KB`); return { ipfsUri: result.ipfsUri, mimeType: result.mimeType, size: result.size, };}
async function uploadToIPFS(piece, options = {}) { const credentials = loadCredentials(); if (!credentials.pinataKey || !credentials.pinataSecret) { throw new Error('❌ Pinata credentials not found in vault'); } console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 📤 Uploading to IPFS via Pinata ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); const pieceName = piece.replace(/^\$/, ''); console.log(`📦 Piece: ${pieceName}`); // Detect content type if not provided let contentType = options.contentType; if (!contentType) { console.log('🔍 Detecting content type...'); const detection = await detectContentType(piece); contentType = detection.type; console.log(` ✓ Detected: ${contentType}`); } // Get bundle from Netlify endpoint (proper self-contained bundle) let bundleHtml; let bundleMeta = {}; if (options.bundleHtml) { bundleHtml = options.bundleHtml; console.log(' Using provided bundle HTML'); } else { const bundle = await fetchBundleFromNetlify(piece, contentType); bundleHtml = bundle.html; bundleMeta = { sourceCode: bundle.sourceCode, authorHandle: bundle.authorHandle, userCode: bundle.userCode, packDate: bundle.packDate, depCount: bundle.depCount, }; } // Use piece name as the unique identifier (simple and deterministic) console.log(`🔐 Piece ID: ${pieceName}`); // Upload to Pinata const formData = new FormData(); const blob = new Blob([bundleHtml], { type: 'text/html' }); formData.append('file', blob, 'index.html'); formData.append('pinataMetadata', JSON.stringify({ name: `aesthetic.computer-keep-${pieceName}` })); formData.append('pinataOptions', JSON.stringify({ wrapWithDirectory: true })); console.log('📤 Uploading to IPFS...'); const response = await fetch(`${CONFIG.pinata.apiUrl}/pinning/pinFileToIPFS`, { method: 'POST', headers: { 'pinata_api_key': credentials.pinataKey, 'pinata_secret_api_key': credentials.pinataSecret }, body: formData }); if (!response.ok) { const error = await response.text(); throw new Error(`Pinata upload failed: ${error}`); } const result = await response.json(); const cid = result.IpfsHash; console.log(`\n✅ Uploaded to IPFS!`); console.log(` CID: ${cid}`); console.log(` Gateway: ${CONFIG.pinata.gateway}/ipfs/${cid}`); console.log(` IPFS URI: ipfs://${cid}\n`); return { cid, contentHash: pieceName, // Use piece name as unique key contentType, gatewayUrl: `${CONFIG.pinata.gateway}/ipfs/${cid}`, ipfsUri: `ipfs://${cid}`, // Bundle metadata for KidLisp pieces ...bundleMeta, };}
// ============================================================================// Mint Token// ============================================================================
// Helper to convert string to hex bytes (TZIP-21 format - raw UTF-8 hex, no pack prefix)function stringToBytes(str) { return Buffer.from(str, 'utf8').toString('hex');}
const KEEP_PERMIT_PAYLOAD_TYPE = { prim: 'pair', args: [ { prim: 'address', annots: ['%contract'] }, { prim: 'pair', args: [ { prim: 'address', annots: ['%owner'] }, { prim: 'pair', args: [ { prim: 'bytes', annots: ['%content_hash'] }, { prim: 'timestamp', annots: ['%permit_deadline'] }, ], }, ], }, ],};
async function loadPermitSigner() { const connectionString = process.env.MONGODB_CONNECTION_STRING; const dbName = process.env.MONGODB_NAME; if (!connectionString || !dbName) return null; const client = new MongoClient(connectionString, { serverSelectionTimeoutMS: 8000, connectTimeoutMS: 8000 }); try { await client.connect(); const secrets = await client.db(dbName).collection('secrets').findOne({ _id: KEEPS_SECRET_ID }); const privateKey = secrets?.keepPermitSignerPrivateKey || secrets?.keepPermitPrivateKey || secrets?.privateKey; if (!privateKey) return null; return new InMemorySigner(privateKey); } catch { return null; } finally { await client.close().catch(() => {}); }}
async function buildKeepPermit({ signer, contractAddress, owner, contentHashBytes, deadlineIso = null }) { if (!contractAddress || !owner || !contentHashBytes) { throw new Error('Missing keep permit fields (contractAddress, owner, contentHashBytes)'); }
const permitDeadline = deadlineIso || new Date(Date.now() + KEEP_PERMIT_TTL_MS).toISOString(); const payloadData = { prim: 'Pair', args: [ { string: contractAddress }, { prim: 'Pair', args: [ { string: owner }, { prim: 'Pair', args: [ { bytes: contentHashBytes }, { string: permitDeadline }, ], }, ], }, ], };
const packed = packDataBytes(payloadData, KEEP_PERMIT_PAYLOAD_TYPE).bytes; const signature = await signer.sign(packed);
return { permit_deadline: permitDeadline, keep_permit: signature.prefixSig, };}
async function mintToken(piece, options = {}) { const { network = 'mainnet', generateThumbnail: shouldGenerateThumbnail = false, recipient = null, skipConfirm = false } = options; const { tezos, credentials, config } = await createTezosClient(network); // Determine owner: recipient if specified, otherwise the server wallet const ownerAddress = recipient || credentials.address; const allCredentials = loadCredentials(); // For Pinata access // Load contract address (network-specific) const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No contract deployed on ${network}. Run: node keeps.mjs deploy ${network}`); } const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); const pieceName = piece.replace(/^\$/, ''); // Fetch contract storage for status info const contract = await tezos.contract.at(contractAddress); const storage = await contract.storage(); const nextTokenId = storage.next_token_id.toNumber(); const keepFee = storage.keep_fee ? storage.keep_fee.toNumber() / 1_000_000 : 0; // Fetch contract balance const contractBalance = await tezos.tz.getBalance(contractAddress); const contractBalanceXTZ = contractBalance.toNumber() / 1_000_000; // Get objkt base URL const objktBase = network === 'mainnet' ? 'https://objkt.com' : 'https://ghostnet.objkt.com'; console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 📜 Keeping a KidLisp Piece ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log('Keep your KidLisp as a unique digital token.\n'); console.log('─────────────────── Piece ───────────────────'); console.log(` Code: $${pieceName}`); console.log(` Preview: https://aesthetic.computer/$${pieceName}`); if (ownerAddress !== credentials.address) { console.log(` Recipient: ${ownerAddress}`); } console.log(` Thumbnail: ${shouldGenerateThumbnail ? 'Animated WebP (via Oven)' : 'Static PNG (HTTP grab)'}`); console.log('\n─────────────────── Contract ───────────────────'); console.log(` Address: ${contractAddress}`); console.log(` Network: ${config.name}`); console.log(` Explorer: ${config.explorer}/${contractAddress}`); console.log(` Collection: ${objktBase}/collection/${contractAddress}`); console.log(` Admin: ${storage.administrator}`); console.log(` Balance: ${contractBalanceXTZ.toFixed(6)} XTZ`); console.log(` Keeps: ${nextTokenId} total`); console.log(` Next ID: #${nextTokenId}`); if (keepFee > 0) { console.log(` Keep Fee: ${keepFee} XTZ`); } console.log('\n─────────────────── Wallet ───────────────────'); console.log(` Address: ${credentials.address}`); console.log(` Wallet: ${credentials.wallet || 'default'}`); const walletBalance = await tezos.tz.getBalance(credentials.address); console.log(` Balance: ${(walletBalance.toNumber() / 1_000_000).toFixed(6)} XTZ\n`); // Confirmation prompt (unless skipped) if (!skipConfirm) { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise(resolve => { rl.question('Keep this piece? (y/N): ', resolve); }); rl.close(); if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') { console.log('\n❌ Cancelled.\n'); process.exit(0); } console.log(''); } // Check for duplicate BEFORE uploading to IPFS console.log('🔍 Checking for duplicates on-chain...'); const duplicate = await checkDuplicatePiece(pieceName, contractAddress, network); if (duplicate.exists) { throw new Error(`Duplicate! $${pieceName} was already kept as token #${duplicate.tokenId}`); } console.log(' ✓ No duplicate found'); // Detect content type let contentType = options.contentType; if (!contentType) { console.log('🔍 Detecting content type...'); const detection = await detectContentType(piece); contentType = detection.type; console.log(` ✓ Detected: ${contentType}`); } console.log(`📁 Content Type: ${contentType}`); // Upload HTML bundle to IPFS if not provided let artifactUri = options.ipfsUri; let artifactCid = options.contentHash; let contentHash = null; // Source-based hash for duplicate prevention let sourceCode = null; let authorHandle = null; let userCode = null; let packDate = null; let depCount = 0; if (!artifactUri) { console.log('\n📤 Uploading HTML bundle to IPFS...'); const upload = await uploadToIPFS(piece, { contentType }); artifactUri = `ipfs://${upload.cid}`; // Use ipfs:// URI for artifact artifactCid = upload.cid; contentHash = upload.contentHash; // Source-based hash for uniqueness contentType = upload.contentType; // Capture bundle metadata for KidLisp pieces sourceCode = upload.sourceCode; authorHandle = upload.authorHandle; userCode = upload.userCode; packDate = upload.packDate; depCount = upload.depCount || 0; } console.log(`🔗 Artifact URI: ${artifactUri}`); console.log(`💾 Artifact CID: ${artifactCid}`); console.log(`🔐 Content Hash: ${contentHash}`); // Build TZIP-21 compliant JSON metadata for objkt // Token name is just the code (e.g., "$roz") const tokenName = `$${pieceName}`; const acUrl = `https://aesthetic.computer/$${pieceName}`; // Build author display name for attributes let authorDisplayName = null; if (authorHandle && authorHandle !== '@anon') { authorDisplayName = authorHandle; } // Description is ONLY the KidLisp source code (clean and simple) const description = sourceCode || `A KidLisp piece preserved on Tezos`; // v9 metadata policy: single canonical tag only const tags = ['KidLisp']; // Generate and upload thumbnail to IPFS if requested let thumbnailUri = `https://grab.aesthetic.computer/preview/400x400/$${pieceName}.png`; // HTTP fallback let thumbnailMimeType = 'image/png'; if (shouldGenerateThumbnail) { try { const thumbnail = await generateThumbnail(piece, allCredentials, { format: 'webp', width: 512, height: 512, duration: 12000, fps: 7.5, playbackFps: 15, quality: 90, }); thumbnailUri = thumbnail.ipfsUri; thumbnailMimeType = thumbnail.mimeType; } catch (error) { console.warn(` ⚠️ Thumbnail generation failed: ${error.message}`); console.warn(` Using HTTP fallback: ${thumbnailUri}`); } } // creators array contains just the wallet address for on-chain attribution // objkt.com uses firstMinter for artist display const creatorsArray = [credentials.address]; const metadataJson = { name: tokenName, description: description, artifactUri: artifactUri, displayUri: artifactUri, thumbnailUri: thumbnailUri, decimals: 0, symbol: 'KEEP', isBooleanAmount: true, shouldPreferSymbol: false, minter: authorHandle || credentials.address, creators: creatorsArray, rights: '© All rights reserved', mintingTool: 'https://kidlisp.com', formats: [{ uri: artifactUri, mimeType: 'text/html', dimensions: { value: 'responsive', unit: 'viewport' } }], tags: tags, attributes: [ { name: 'Language', value: 'KidLisp' }, { name: 'Code', value: `$${pieceName}` }, ...(authorDisplayName ? [{ name: 'Author', value: authorDisplayName }] : []), ...(userCode ? [{ name: 'User', value: userCode }] : []), ...(sourceCode ? [{ name: 'Lines of Code', value: String(sourceCode.split('\n').length) }] : []), ...(packDate ? [{ name: 'Packed on', value: packDate }] : []), { name: 'Interactive', value: 'Yes' }, { name: 'Platform', value: 'Aesthetic Computer' }, ] }; // Upload JSON metadata to IPFS console.log('\n📤 Uploading JSON metadata to IPFS...'); const metadataUri = await uploadJsonToIPFS( metadataJson, `aesthetic.computer-keep-${pieceName}-metadata`, allCredentials ); console.log(`📋 Metadata URI: ${metadataUri}`); // For the contract, we only need to store the metadata URI in the "" key // All other fields will be fetched by indexers from the JSON const onChainMetadata = { name: stringToBytes(tokenName), description: stringToBytes(description), artifactUri: stringToBytes(artifactUri), displayUri: stringToBytes(artifactUri), thumbnailUri: stringToBytes(thumbnailUri), decimals: stringToBytes('0'), symbol: stringToBytes('KEEP'), isBooleanAmount: stringToBytes('true'), shouldPreferSymbol: stringToBytes('false'), formats: stringToBytes(JSON.stringify(metadataJson.formats)), tags: stringToBytes(JSON.stringify(metadataJson.tags)), attributes: stringToBytes(JSON.stringify(metadataJson.attributes)), creators: stringToBytes(JSON.stringify(creatorsArray)), rights: stringToBytes('© All rights reserved'), content_type: stringToBytes('KidLisp'), content_hash: stringToBytes(contentHash), // Source-based hash for uniqueness // IMPORTANT: This is the off-chain metadata URI that objkt will fetch metadata_uri: stringToBytes(metadataUri), }; // Call keep entrypoint console.log('\n📤 Preserving on Tezos blockchain...');
try { // Use the backend permit signer key (from MongoDB), not the wallet key const permitSigner = await loadPermitSigner() || tezos.signer; const keepPermit = await buildKeepPermit({ signer: permitSigner, contractAddress, owner: ownerAddress, contentHashBytes: onChainMetadata.content_hash, });
// Build royalties JSON (objkt standard: decimals 4, shares in bps) const storage = await contract.storage(); const artistBps = storage.artist_royalty_bps ? storage.artist_royalty_bps.toNumber() : (storage.default_royalty_bps ? storage.default_royalty_bps.toNumber() : 1000); const platformBps = storage.platform_royalty_bps ? storage.platform_royalty_bps.toNumber() : 0; const treasuryAddr = storage.treasury_address || null; const royaltiesObj = { decimals: 4, shares: { [ownerAddress]: String(artistBps) } }; if (platformBps > 0 && treasuryAddr) royaltiesObj.shares[treasuryAddr] = String(platformBps); const royaltiesBytes = stringToBytes(JSON.stringify(royaltiesObj));
const op = await contract.methodsObject.keep({ artifactUri: onChainMetadata.artifactUri, content_hash: onChainMetadata.content_hash, creators: onChainMetadata.creators, decimals: onChainMetadata.decimals, description: onChainMetadata.description, displayUri: onChainMetadata.displayUri, metadata_uri: onChainMetadata.metadata_uri, name: onChainMetadata.name, owner: ownerAddress, royalties: royaltiesBytes, symbol: onChainMetadata.symbol, thumbnailUri: onChainMetadata.thumbnailUri, permit_deadline: keepPermit.permit_deadline, keep_permit: keepPermit.keep_permit, }).send(); console.log(` ⏳ Operation hash: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1); // Get the token ID from contract storage (next_token_id - 1) // This is O(1) and scales to millions of tokens const updatedStorage = await contract.storage(); const tokenId = updatedStorage.next_token_id.toNumber() - 1; console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ ✅ Piece Kept Successfully! ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(` 🎨 Token ID: #${tokenId}`); console.log(` 📦 Piece: $${pieceName}`); console.log(` 🔗 Artifact: ${artifactUri}`); console.log(` 📝 Operation: ${config.explorer}/${op.hash}`); console.log(` 🖼️ View on Objkt: ${objktBase}/asset/${contractAddress}/${tokenId}\n`); return { tokenId, hash: op.hash, artifactUri }; } catch (error) { console.error('\n❌ Keep failed!'); console.error(` Error: ${error.message}`); throw error; }}
// ============================================================================// Update Metadata// ============================================================================
async function updateMetadata(tokenId, piece, options = {}) { const { network = 'mainnet', generateThumbnail: shouldGenerateThumbnail = false } = options; const { tezos, credentials, config } = await createTezosClient(network); const allCredentials = loadCredentials(); // For Pinata access // Load contract address (network-specific) const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No contract deployed on ${network}. Run: node keeps.mjs deploy ${network}`); } const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); const pieceName = piece.replace(/^\$/, ''); const acUrl = `https://aesthetic.computer/$${pieceName}`; console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🔄 Updating Token Metadata ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${contractAddress}`); console.log(`🎨 Token ID: ${tokenId}`); console.log(`📦 Piece: ${pieceName}`); // Detect content type console.log('🔍 Detecting content type...'); const detection = await detectContentType(piece); const contentType = detection.type; console.log(` ✓ Detected: ${contentType}`); // Upload new bundle to IPFS (skip duplicate check since we're updating) console.log('\n📤 Uploading new bundle to IPFS...'); const upload = await uploadToIPFS(piece, { contentType, skipDuplicateCheck: true }); const artifactUri = `ipfs://${upload.cid}`; const artifactCid = upload.cid; // Get bundle metadata const sourceCode = upload.sourceCode; const authorHandle = upload.authorHandle; const userCode = upload.userCode; const packDate = upload.packDate; const depCount = upload.depCount || 0; console.log(`🔗 New Artifact URI: ${artifactUri}`); // Build author display name for attributes let authorDisplayName = null; if (authorHandle && authorHandle !== '@anon') { authorDisplayName = authorHandle; } // Description is ONLY the KidLisp source code (clean and simple) const description = sourceCode || `A KidLisp piece preserved on Tezos`; // v9 metadata policy: single canonical tag only const tags = ['KidLisp']; // Build improved attributes const attributes = [ { name: 'Language', value: 'KidLisp' }, { name: 'Code', value: `$${pieceName}` }, ...(authorDisplayName ? [{ name: 'Author', value: authorDisplayName }] : []), ...(userCode ? [{ name: 'User', value: userCode }] : []), ...(sourceCode ? [{ name: 'Lines of Code', value: String(sourceCode.split('\n').length) }] : []), ...(packDate ? [{ name: 'Packed on', value: packDate }] : []), { name: 'Interactive', value: 'Yes' }, { name: 'Platform', value: 'Aesthetic Computer' }, ]; // Preserve the ORIGINAL creator from firstMinter on TzKT // This ensures artist attribution is maintained on updates let originalCreator = credentials.address; // fallback to current wallet try { const tzktBase = network === 'mainnet' ? 'https://api.tzkt.io' : `https://api.${network}.tzkt.io`; const tokenUrl = `${tzktBase}/v1/tokens?contract=${contractAddress}&tokenId=${tokenId}`; const tokenResponse = await fetch(tokenUrl); if (tokenResponse.ok) { const tokens = await tokenResponse.json(); if (tokens[0]?.firstMinter?.address) { originalCreator = tokens[0].firstMinter.address; console.log(` ✓ Preserving original creator: ${originalCreator}`); } } } catch (e) { console.warn(` ⚠ Could not fetch original creator, using current wallet`); } const creatorsArray = [originalCreator]; // Generate thumbnail via oven if requested, otherwise use HTTP fallback let thumbnailUri = `https://grab.aesthetic.computer/preview/400x400/$${pieceName}.png`; if (shouldGenerateThumbnail) { try { const thumbnail = await generateThumbnail(pieceName, allCredentials, { width: 256, height: 256, duration: 8000, fps: 10, playbackFps: 20, density: 2, quality: 70, }); thumbnailUri = thumbnail.ipfsUri; console.log(` 🖼️ Thumbnail: ${thumbnailUri}`); } catch (err) { console.warn(` ⚠ Thumbnail generation failed, using HTTP fallback: ${err.message}`); } } // Build metadata JSON for IPFS const tokenName = `$${pieceName}`; const metadataJson = { name: tokenName, description: description, artifactUri: artifactUri, displayUri: artifactUri, thumbnailUri: thumbnailUri, decimals: 0, symbol: 'KEEP', isBooleanAmount: true, shouldPreferSymbol: false, minter: authorHandle || credentials.address, creators: creatorsArray, rights: '© All rights reserved', mintingTool: 'https://kidlisp.com', formats: [{ uri: artifactUri, mimeType: 'text/html', dimensions: { value: 'responsive', unit: 'viewport' } }], tags: tags, attributes: attributes }; // Upload JSON metadata to IPFS console.log('\n📤 Uploading JSON metadata to IPFS...'); const metadataUri = await uploadJsonToIPFS( metadataJson, `aesthetic.computer-keep-${pieceName}-metadata-updated`, allCredentials ); console.log(`📋 Metadata URI: ${metadataUri}`); // Build on-chain token_info const tokenInfo = { name: stringToBytes(tokenName), description: stringToBytes(description), artifactUri: stringToBytes(artifactUri), displayUri: stringToBytes(artifactUri), thumbnailUri: stringToBytes(metadataJson.thumbnailUri), decimals: stringToBytes('0'), symbol: stringToBytes('KEEP'), isBooleanAmount: stringToBytes('true'), shouldPreferSymbol: stringToBytes('false'), formats: stringToBytes(JSON.stringify(metadataJson.formats)), tags: stringToBytes(JSON.stringify(tags)), attributes: stringToBytes(JSON.stringify(attributes)), creators: stringToBytes(JSON.stringify(creatorsArray)), rights: stringToBytes('© All rights reserved'), content_type: stringToBytes('KidLisp'), content_hash: stringToBytes(pieceName), '': stringToBytes(metadataUri) }; // Call edit_metadata entrypoint console.log('\n📤 Calling edit_metadata entrypoint...'); try { const contract = await tezos.contract.at(contractAddress); const op = await contract.methodsObject.edit_metadata({ token_id: tokenId, token_info: tokenInfo }).send(); console.log(` ⏳ Operation hash: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ ✅ Metadata Updated Successfully! ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(` 🎨 Token ID: ${tokenId}`); console.log(` 🔗 New Artifact: ${artifactUri}`); console.log(` 📝 Operation: ${config.explorer}/${op.hash}\n`); return { tokenId, hash: op.hash, artifactUri }; } catch (error) { console.error('\n❌ Update failed!'); console.error(` Error: ${error.message}`); if (error.message.includes('METADATA_LOCKED')) { console.error('\n 💡 This token\'s metadata has been locked and cannot be updated.'); } throw error; }}
// ============================================================================// Redact Token (Censor)// ============================================================================
async function redactToken(tokenId, options = {}) { const { network = 'mainnet', reason = 'Content has been redacted.' } = options; const { tezos, credentials, config } = await createTezosClient(network); const allCredentials = loadCredentials(); // Load contract address (network-specific) const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No contract deployed on ${network}. Run: node keeps.mjs deploy ${network}`); } const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🚫 Redacting Token ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${contractAddress}`); console.log(`🎨 Token ID: ${tokenId}`); console.log(`📝 Reason: ${reason}`); console.log('\n⚠️ This will replace all content with a redacted placeholder.\n'); // Generate a red "REDACTED" image console.log('🖼️ Generating redacted thumbnail...'); // Create a simple red HTML page for the artifact const redactedHtml = `<!DOCTYPE html><html><head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>REDACTED</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } body { background: #1a0000; color: #ff0000; font-family: monospace; display: flex; align-items: center; justify-content: center; min-height: 100vh; text-align: center; } .container { padding: 2rem; } h1 { font-size: 3rem; letter-spacing: 0.5em; margin-bottom: 1rem; text-shadow: 0 0 20px #ff0000; } p { font-size: 1rem; opacity: 0.7; } .bars { display: flex; flex-direction: column; gap: 8px; margin-top: 2rem; } .bar { height: 20px; background: #ff0000; opacity: 0.3; } .bar:nth-child(1) { width: 80%; } .bar:nth-child(2) { width: 60%; } .bar:nth-child(3) { width: 90%; } .bar:nth-child(4) { width: 45%; } </style></head><body> <div class="container"> <h1>REDACTED</h1> <p>${reason}</p> <div class="bars"> <div class="bar"></div> <div class="bar"></div> <div class="bar"></div> <div class="bar"></div> </div> </div></body></html>`;
// Upload redacted HTML to IPFS console.log('📤 Uploading redacted artifact to IPFS...'); const formData = new FormData(); const blob = new Blob([redactedHtml], { type: 'text/html' }); formData.append('file', blob, 'index.html'); formData.append('pinataMetadata', JSON.stringify({ name: `aesthetic.computer-redacted-${tokenId}` })); formData.append('pinataOptions', JSON.stringify({ wrapWithDirectory: true })); const uploadResponse = await fetch(`${CONFIG.pinata.apiUrl}/pinning/pinFileToIPFS`, { method: 'POST', headers: { 'pinata_api_key': allCredentials.pinataKey, 'pinata_secret_api_key': allCredentials.pinataSecret }, body: formData }); if (!uploadResponse.ok) { throw new Error(`Failed to upload redacted artifact: ${await uploadResponse.text()}`); } const uploadResult = await uploadResponse.json(); const artifactCid = uploadResult.IpfsHash; const artifactUri = `ipfs://${artifactCid}`; console.log(` ✓ Artifact: ${artifactUri}`); // Generate red thumbnail via Oven console.log('📸 Generating redacted thumbnail via Oven...'); let thumbnailUri = 'https://grab.aesthetic.computer/preview/400x400/redacted.png'; try { // Use oven to capture the redacted page const ovenResponse = await fetch(`${CONFIG.oven.url}/grab-ipfs`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: `${CONFIG.pinata.gateway}/ipfs/${artifactCid}`, format: 'webp', width: 96, height: 96, density: 2, duration: 1000, fps: 1, quality: 80, pinataKey: allCredentials.pinataKey, pinataSecret: allCredentials.pinataSecret, }), }); if (ovenResponse.ok) { const ovenResult = await ovenResponse.json(); if (ovenResult.success) { thumbnailUri = ovenResult.ipfsUri; console.log(` ✓ Thumbnail: ${thumbnailUri}`); } } } catch (e) { console.log(` ⚠️ Thumbnail generation failed, using fallback`); } // Build redacted metadata const tokenName = '[REDACTED]'; const description = `[REDACTED]\n\n${reason}`; const tags = ['REDACTED', 'censored']; const attributes = [ { name: 'Status', value: 'REDACTED' }, { name: 'Reason', value: reason }, { name: 'Platform', value: 'Aesthetic Computer' }, ]; // For redacted content, use admin wallet as creator (censorship action) const creatorsArray = [credentials.address]; // Upload metadata JSON const metadataJson = { name: tokenName, description: description, artifactUri: artifactUri, displayUri: artifactUri, thumbnailUri: thumbnailUri, decimals: 0, symbol: 'KEEP', isBooleanAmount: true, shouldPreferSymbol: false, minter: '@aesthetic', creators: creatorsArray, rights: '© All rights reserved', mintingTool: 'https://kidlisp.com', formats: [{ uri: artifactUri, mimeType: 'text/html', dimensions: { value: 'responsive', unit: 'viewport' } }], tags: tags, attributes: attributes }; console.log('📤 Uploading redacted metadata to IPFS...'); const metadataUri = await uploadJsonToIPFS( metadataJson, `aesthetic.computer-redacted-${tokenId}-metadata`, allCredentials ); console.log(` ✓ Metadata: ${metadataUri}`); // Build on-chain token_info const tokenInfo = { name: stringToBytes(tokenName), description: stringToBytes(description), artifactUri: stringToBytes(artifactUri), displayUri: stringToBytes(artifactUri), thumbnailUri: stringToBytes(thumbnailUri), decimals: stringToBytes('0'), symbol: stringToBytes('KEEP'), isBooleanAmount: stringToBytes('true'), shouldPreferSymbol: stringToBytes('false'), formats: stringToBytes(JSON.stringify(metadataJson.formats)), tags: stringToBytes(JSON.stringify(tags)), attributes: stringToBytes(JSON.stringify(attributes)), creators: stringToBytes(JSON.stringify(creatorsArray)), rights: stringToBytes('© All rights reserved'), content_type: stringToBytes('REDACTED'), content_hash: stringToBytes('REDACTED'), '': stringToBytes(metadataUri) }; // Call edit_metadata entrypoint console.log('\n📤 Calling edit_metadata entrypoint...'); try { const contract = await tezos.contract.at(contractAddress); const op = await contract.methodsObject.edit_metadata({ token_id: tokenId, token_info: tokenInfo }).send(); console.log(` ⏳ Operation hash: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🚫 Token Redacted Successfully! ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(` 🎨 Token ID: ${tokenId}`); console.log(` 🚫 Status: REDACTED`); console.log(` 📝 Operation: ${config.explorer}/${op.hash}\n`); return { tokenId, hash: op.hash, redacted: true }; } catch (error) { console.error('\n❌ Redaction failed!'); console.error(` Error: ${error.message}`); if (error.message.includes('METADATA_LOCKED')) { console.error('\n 💡 This token\'s metadata has been locked and cannot be redacted.'); } throw error; }}
// ============================================================================// Set Collection Media (Contract-level Metadata)// ============================================================================
async function setCollectionMedia(options = {}) { const { network = 'mainnet', name, // Collection name imageUri, // Collection icon/logo (IPFS URI or URL) homepage, // Collection homepage URL description, // Collection description raw = {} // Raw key-value pairs to set } = options; const { tezos, credentials, config } = await createTezosClient(network); // Load contract address (network-specific) const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No contract deployed on ${network}. Run: node keeps.mjs deploy ${network}`); } const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🎨 Setting Collection Media ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${contractAddress}\n`); // Build the metadata updates const updates = []; const contract = await tezos.contract.at(contractAddress);
// Load existing contract metadata so partial updates don't wipe fields. let existingMetadata = {}; try { const storage = await contract.storage(); const existingContent = await storage.metadata.get('content'); const decoded = decodeContractMetadataBytes(existingContent); if (decoded && typeof decoded === 'object') { existingMetadata = decoded; } } catch (error) { console.warn(` ⚠️ Could not read existing metadata, using defaults: ${error.message}`); }
// Build new contract metadata JSON (merge existing + updates) const currentMetadata = { ...existingMetadata, name: name || existingMetadata.name || "KidLisp Keeps", version: existingMetadata.version || "2.0.0", interfaces: existingMetadata.interfaces || ["TZIP-012", "TZIP-016", "TZIP-021"], authors: existingMetadata.authors || ["aesthetic.computer"], homepage: homepage || existingMetadata.homepage || "https://keep.kidlisp.com" }; if (options.name) { console.log(` 📛 Name: ${options.name}`); } if (imageUri) { currentMetadata.imageUri = imageUri; currentMetadata.thumbnailUri = imageUri; console.log(` 🖼️ Image URI: ${imageUri}`); } if (description) { currentMetadata.description = description; console.log(` 📝 Description: ${description.substring(0, 80)}...`); }
if (homepage) { console.log(` 🏠 Homepage: ${homepage}`); } // Add any raw fields for (const [key, value] of Object.entries(raw)) { currentMetadata[key] = value; console.log(` 📦 ${key}: ${String(value).substring(0, 50)}`); } // Update the "content" key with new metadata JSON const metadataJson = JSON.stringify(currentMetadata); const metadataBytes = stringToBytes(metadataJson); updates.push({ key: 'content', value: metadataBytes }); console.log(`\n📤 Updating contract metadata...`); try { // Format for set_contract_metadata: list of { key: string, value: bytes } // Bytes must be hex string prefixed with 0x for Taquito const params = updates.map(u => ({ key: u.key, value: '0x' + u.value })); const op = await pickMethod(contract, 'set_contract_metadata')(params).send(); console.log(` ⏳ Operation hash: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ ✅ Collection Media Updated! ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(` 📝 Operation: ${config.explorer}/${op.hash}\n`); return { hash: op.hash, metadata: currentMetadata }; } catch (error) { console.error('\n❌ Update failed!'); console.error(` Error: ${error.message}`); throw error; }}
// ============================================================================// Lock Collection Metadata// ============================================================================
async function lockCollectionMetadata(options = {}) { const { network = 'mainnet' } = options; const { tezos, credentials, config } = await createTezosClient(network); // Load contract address (network-specific) const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No contract deployed on ${network}. Run: node keeps.mjs deploy ${network}`); } const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🔒 Locking Collection Metadata ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${contractAddress}`); console.log('\n⚠️ WARNING: This action is PERMANENT.'); console.log(' Collection metadata (name, description, image) cannot be updated after locking.\n'); try { const contract = await tezos.contract.at(contractAddress); const op = await pickMethod(contract, 'lock_contract_metadata')().send(); console.log(` ⏳ Operation hash: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ ✅ Collection Metadata Locked! ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(` 🔒 Status: PERMANENTLY LOCKED`); console.log(` 📝 Operation: ${config.explorer}/${op.hash}\n`); return { hash: op.hash, locked: true }; } catch (error) { console.error('\n❌ Lock failed!'); console.error(` Error: ${error.message}`); throw error; }}
// ============================================================================// Lock Metadata// ============================================================================
async function lockMetadata(tokenId, options = {}) { const { network = 'mainnet' } = options; const { tezos, credentials, config } = await createTezosClient(network); // Load contract address (network-specific) const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No contract deployed on ${network}. Run: node keeps.mjs deploy ${network}`); } const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🔒 Locking Token Metadata ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${contractAddress}`); console.log(`🎨 Token ID: ${tokenId}`); console.log('\n⚠️ WARNING: This action is PERMANENT. Metadata cannot be updated after locking.\n'); try { const contract = await tezos.contract.at(contractAddress); const op = await pickMethod(contract, 'lock_metadata')(tokenId).send(); console.log(` ⏳ Operation hash: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ ✅ Metadata Locked Successfully! ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(` 🎨 Token ID: ${tokenId}`); console.log(` 🔒 Status: PERMANENTLY LOCKED`); console.log(` 📝 Operation: ${config.explorer}/${op.hash}\n`); return { tokenId, hash: op.hash, locked: true }; } catch (error) { console.error('\n❌ Lock failed!'); console.error(` Error: ${error.message}`); throw error; }}
// ============================================================================// Burn Token// ============================================================================
async function burnToken(tokenId, options = {}) { const { network = 'mainnet' } = options; const { tezos, credentials, config } = await createTezosClient(network); // Load contract address (network-specific) const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No contract deployed on ${network}. Run: node keeps.mjs deploy ${network}`); } const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🔥 Burning Token ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${contractAddress}`); console.log(`🎨 Token ID: ${tokenId}`); console.log('\n⚠️ WARNING: This action is PERMANENT. The token will be destroyed.\n'); console.log(' The piece name will become available for re-keeping.\n'); try { const contract = await tezos.contract.at(contractAddress); const op = await pickMethod(contract, 'burn_keep')(tokenId).send(); console.log(` ⏳ Operation hash: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ ✅ Token Burned Successfully! ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(` 🔥 Token ID: ${tokenId} - DESTROYED`); console.log(` 📝 Operation: ${config.explorer}/${op.hash}\n`); return { tokenId, hash: op.hash, burned: true }; } catch (error) { console.error('\n❌ Burn failed!'); console.error(` Error: ${error.message}`); throw error; }}
// ============================================================================// Fee Management// ============================================================================
async function getKeepFee(network = 'mainnet') { const { tezos, config } = await createTezosClient(network); const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No contract deployed on ${network}. Run: node keeps.mjs deploy ${network}`); } const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); const contract = await tezos.contract.at(contractAddress); const storage = await contract.storage(); // keep_fee is stored in mutez const feeInMutez = storage.keep_fee?.toNumber?.() ?? storage.keep_fee ?? 0; const feeInTez = feeInMutez / 1_000_000; return { feeInMutez, feeInTez, contractAddress };}
async function setKeepFee(feeInTez, options = {}) { const { network = 'mainnet' } = options; const { tezos, config } = await createTezosClient(network); const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No contract deployed on ${network}. Run: node keeps.mjs deploy ${network}`); } const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); const feeInMutez = Math.floor(feeInTez * 1_000_000); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 💰 Setting Keep Fee ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${contractAddress}`); console.log(`💵 New Fee: ${feeInTez} XTZ (${feeInMutez} mutez)\n`); try { const contract = await tezos.contract.at(contractAddress); const op = await pickMethod(contract, 'set_keep_fee')(feeInMutez).send(); console.log(` ⏳ Operation hash: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ ✅ Keep Fee Updated Successfully! ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(` 💵 New keep fee: ${feeInTez} XTZ`); console.log(` 📝 Operation: ${config.explorer}/${op.hash}\n`); return { feeInTez, feeInMutez, hash: op.hash }; } catch (error) { console.error('\n❌ Set fee failed!'); console.error(` Error: ${error.message}`); throw error; }}
async function setAdministrator(newAdmin, options = {}) { const { network = 'mainnet' } = options; const { tezos, config } = await createTezosClient(network); const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No contract deployed on ${network}. Run: node keeps.mjs deploy ${network}`); } const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 👑 Setting Contract Administrator ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${contractAddress}`); console.log(`👤 New Admin: ${newAdmin}\n`); try { const contract = await tezos.contract.at(contractAddress); const op = await pickMethod(contract, 'set_administrator')(newAdmin).send(); console.log(` ⏳ Operation hash: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ ✅ Administrator Changed Successfully! ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(` 👤 New admin: ${newAdmin}`); console.log(` 📝 Operation: ${config.explorer}/${op.hash}\n`); console.log(' ⚠️ WARNING: Only the new admin can call admin functions now!\n'); return { newAdmin, hash: op.hash }; } catch (error) { console.error('\n❌ Set administrator failed!'); console.error(` Error: ${error.message}`); throw error; }}
async function withdrawFees(destination, options = {}) { const { network = 'mainnet' } = options; const { tezos, credentials, config } = await createTezosClient(network); const addressPath = getContractAddressPath(network); if (!fs.existsSync(addressPath)) { throw new Error(`❌ No contract deployed on ${network}. Run: node keeps.mjs deploy ${network}`); } const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); const dest = destination || credentials.address; // Default to admin address // Check contract balance first const contractBalance = await tezos.tz.getBalance(contractAddress); const balanceXTZ = contractBalance.toNumber() / 1_000_000; console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 💸 Withdrawing Fees ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${contractAddress}`); console.log(`💰 Contract Balance: ${balanceXTZ.toFixed(6)} XTZ`); console.log(`📤 Destination: ${dest}\n`); if (balanceXTZ === 0) { console.log(' ℹ️ No fees to withdraw (balance is 0)\n'); return { withdrawn: 0, hash: null }; } try { const contract = await tezos.contract.at(contractAddress); const op = await pickMethod(contract, 'withdraw_fees')(dest).send(); console.log(` ⏳ Operation hash: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ ✅ Fees Withdrawn Successfully! ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(` 💸 Withdrawn: ${balanceXTZ.toFixed(6)} XTZ`); console.log(` 📤 To: ${dest}`); console.log(` 📝 Operation: ${config.explorer}/${op.hash}\n`); return { withdrawn: balanceXTZ, destination: dest, hash: op.hash }; } catch (error) { console.error('\n❌ Withdrawal failed!'); console.error(` Error: ${error.message}`); throw error; }}
// ============================================================================// v4 NEW FEATURES - Royalty, Pause, Admin Transfer// ============================================================================
async function getRoyalty(network = 'mainnet') { console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🎨 Current Royalty Configuration ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
const { tezos, config } = await createTezosClient(network); const addressPath = getContractAddressPath(network);
if (!fs.existsSync(addressPath)) { throw new Error(`No contract address found for ${network}. Deploy contract first.`); }
const contractAddress = fs.readFileSync(addressPath, 'utf8').trim(); const contract = await tezos.contract.at(contractAddress); const storage = await contract.storage();
const royaltyBps = storage.default_royalty_bps ? storage.default_royalty_bps.toNumber() : 1000; const royaltyPercent = (royaltyBps / 100).toFixed(1);
console.log(` Contract: ${contractAddress}`); console.log(` Network: ${config.name}`); console.log(` Royalty: ${royaltyPercent}% (${royaltyBps} basis points)`); console.log(` Explorer: ${config.explorer}/${contractAddress}\n`);
return { contractAddress, royaltyBps, royaltyPercent };}
async function setRoyalty(percentage, options = {}) { const network = options.network || 'mainnet';
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🎨 Setting Default Royalty ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
if (percentage < 0 || percentage > 25) { throw new Error('Royalty must be between 0% and 25%'); }
const bps = Math.round(percentage * 100); // Convert to basis points
const { tezos, credentials, config } = await createTezosClient(network); const addressPath = getContractAddressPath(network);
if (!fs.existsSync(addressPath)) { throw new Error(`No contract address found for ${network}. Deploy contract first.`); }
const contractAddress = fs.readFileSync(addressPath, 'utf8').trim();
console.log(` Setting royalty to ${percentage}% (${bps} basis points)...`); console.log(` Contract: ${contractAddress}`); console.log(` Admin: ${credentials.address}\n`);
const contract = await tezos.contract.at(contractAddress); const op = await contract.methodsObject.set_default_royalty(bps).send();
console.log(` Transaction: ${op.hash}`); console.log(` Waiting for confirmation...`);
await op.confirmation(1);
console.log(`\n✅ Royalty set to ${percentage}%`); console.log(`🔗 ${config.explorer}/${op.hash}\n`);}
async function pauseContract(options = {}) { const network = options.network || 'mainnet';
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🚨 EMERGENCY PAUSE ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
const { tezos, credentials, config } = await createTezosClient(network); const addressPath = getContractAddressPath(network);
if (!fs.existsSync(addressPath)) { throw new Error(`No contract address found for ${network}. Deploy contract first.`); }
const contractAddress = fs.readFileSync(addressPath, 'utf8').trim();
console.log(` ⚠️ This will stop all minting and metadata edits`); console.log(` Contract: ${contractAddress}`); console.log(` Admin: ${credentials.address}\n`);
// Confirmation prompt const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise(resolve => { rl.question('Pause contract? (y/N): ', resolve); }); rl.close();
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') { console.log('\n❌ Cancelled.\n'); return; }
const contract = await tezos.contract.at(contractAddress); const op = await contract.methodsObject.pause().send();
console.log(`\n Transaction: ${op.hash}`); console.log(` Waiting for confirmation...`);
await op.confirmation(1);
console.log(`\n✅ Contract PAUSED`); console.log(`🔗 ${config.explorer}/${op.hash}\n`); console.log(`⚠️ Minting and metadata edits are now disabled`); console.log(` Use "node keeps.mjs unpause" to resume operations\n`);}
async function unpauseContract(options = {}) { const network = options.network || 'mainnet';
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ ✅ UNPAUSE CONTRACT ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
const { tezos, credentials, config } = await createTezosClient(network); const addressPath = getContractAddressPath(network);
if (!fs.existsSync(addressPath)) { throw new Error(`No contract address found for ${network}. Deploy contract first.`); }
const contractAddress = fs.readFileSync(addressPath, 'utf8').trim();
console.log(` Resuming normal operations...`); console.log(` Contract: ${contractAddress}`); console.log(` Admin: ${credentials.address}\n`);
const contract = await tezos.contract.at(contractAddress); const op = await contract.methodsObject.unpause().send();
console.log(` Transaction: ${op.hash}`); console.log(` Waiting for confirmation...`);
await op.confirmation(1);
console.log(`\n✅ Contract UNPAUSED`); console.log(`🔗 ${config.explorer}/${op.hash}\n`); console.log(` Minting and metadata edits are now enabled\n`);}
async function adminTransfer(tokenId, fromAddress, toAddress, options = {}) { const network = options.network || 'mainnet';
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🔄 Admin Emergency Transfer ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
const { tezos, credentials, config } = await createTezosClient(network); const addressPath = getContractAddressPath(network);
if (!fs.existsSync(addressPath)) { throw new Error(`No contract address found for ${network}. Deploy contract first.`); }
const contractAddress = fs.readFileSync(addressPath, 'utf8').trim();
console.log(` Token ID: #${tokenId}`); console.log(` From: ${fromAddress}`); console.log(` To: ${toAddress}`); console.log(` Contract: ${contractAddress}`); console.log(` Admin: ${credentials.address}\n`);
// Confirmation prompt const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); const answer = await new Promise(resolve => { rl.question('Transfer token? (y/N): ', resolve); }); rl.close();
if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') { console.log('\n❌ Cancelled.\n'); return; }
const contract = await tezos.contract.at(contractAddress); const op = await contract.methodsObject.admin_transfer({ token_id: tokenId, from_: fromAddress, to_: toAddress }).send();
console.log(`\n Transaction: ${op.hash}`); console.log(` Waiting for confirmation...`);
await op.confirmation(1);
console.log(`\n✅ Token transferred`); console.log(`🔗 ${config.explorer}/${op.hash}`); console.log(`📊 ${config.explorer}/${contractAddress}/tokens/${tokenId}\n`);}
// ============================================================================// Send XTZ// ============================================================================
async function sendTez(toAddress, amount, network = 'mainnet') { const { tezos, credentials, config } = await createTezosClient(network);
let resolvedAddress = toAddress; if (toAddress.endsWith('.tez')) { console.log(`\n🔍 Resolving ${toAddress}...`); resolvedAddress = await resolveTezDomain(toAddress); console.log(` → ${resolvedAddress}`); }
const mutez = Math.floor(amount * 1_000_000);
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 💸 Send XTZ ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
console.log(`📡 Network: ${config.name}`); console.log(`📤 From: ${credentials.address}`); console.log(`📥 To: ${resolvedAddress}${toAddress !== resolvedAddress ? ` (${toAddress})` : ''}`); console.log(`💵 Amount: ${amount} XTZ\n`);
const op = await tezos.contract.transfer({ to: resolvedAddress, amount: mutez, mutez: true });
console.log(` ⏳ Operation: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1);
console.log('\n✅ Sent!'); console.log(` 🔗 ${config.explorer}/${op.hash}\n`); return { hash: op.hash };}
// ============================================================================// FA2 Transfer// ============================================================================
async function resolveTezDomain(name) { const resp = await fetch(`https://api.tzkt.io/v1/domains?name=${encodeURIComponent(name)}`); if (!resp.ok) throw new Error(`Tezos Domains lookup failed: ${resp.status}`); const data = await resp.json(); const entry = data?.[0]; if (!entry?.owner?.address) throw new Error(`Could not resolve ${name}`); return entry.owner.address;}
async function transferToken(tokenId, toAddress, network = 'mainnet') { const { tezos, credentials, config } = await createTezosClient(network); const contractAddress = loadContractAddress(network);
let resolvedAddress = toAddress; if (toAddress.endsWith('.tez')) { console.log(`\n🔍 Resolving ${toAddress}...`); resolvedAddress = await resolveTezDomain(toAddress); console.log(` → ${resolvedAddress}`); }
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🎁 Transfer Token ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${contractAddress}`); console.log(`🎨 Token: #${tokenId}`); console.log(`📤 From: ${credentials.address}`); console.log(`📥 To: ${resolvedAddress}${toAddress !== resolvedAddress ? ` (${toAddress})` : ''}\n`);
// Check for active listing and retract if needed const listing = await loadActiveListingForToken(contractAddress, tokenId, credentials.address); const contract = await tezos.contract.at(contractAddress);
if (listing) { const askId = Number.parseInt(String(listing.bigmap_key), 10) || listing.id; const askPrice = (Number(listing.price_xtz || 0) / 1_000_000).toFixed(6); console.log(`♻️ Retracting active listing #${askId} (${askPrice} XTZ)...`);
const marketplaceContract = listing.marketplace_contract || OBJKT_MARKETPLACE_FALLBACK[network]; const marketContract = await tezos.contract.at(marketplaceContract);
const op = await tezos.contract.batch() .withContractCall(pickMethod(marketContract, 'retract_ask')(Number(askId))) .withContractCall(pickMethod(contract, 'transfer')([{ from_: credentials.address, txs: [{ to_: resolvedAddress, token_id: tokenId, amount: 1 }] }])) .send();
console.log(` ⏳ Operation: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1);
console.log('\n✅ Listing retracted + token transferred!'); console.log(` 🔗 ${config.explorer}/${op.hash}\n`); return { hash: op.hash, retracted: askId }; }
const op = await pickMethod(contract, 'transfer')([{ from_: credentials.address, txs: [{ to_: resolvedAddress, token_id: tokenId, amount: 1 }] }]).send();
console.log(` ⏳ Operation: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1);
console.log('\n✅ Token transferred!'); console.log(` 🔗 ${config.explorer}/${op.hash}\n`); return { hash: op.hash };}
// ============================================================================// Marketplace Commands (Objkt)// ============================================================================
async function listOwnedTokens(network = 'mainnet', options = {}) { const { credentials, config } = await createTezosClient(network); const contractAddress = loadContractAddress(network); const limit = Number.isFinite(Number(options.limit)) ? Math.max(1, Number(options.limit)) : 200;
const apiBase = tzktApiBase(network); const url = `${apiBase}/v1/tokens/balances?token.contract=${contractAddress}&account=${credentials.address}&balance.gt=0&limit=${limit}`; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to load owned tokens (${response.status})`); }
const rows = await response.json();
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🧾 Wallet Token Inventory ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${contractAddress}`); console.log(`👤 Wallet: ${credentials.address}`); console.log(`📦 Tokens: ${rows.length}\n`);
if (!rows.length) { console.log(' (No tokens in this wallet for current keeps contract)\n'); return []; }
const objktBase = network === 'mainnet' ? 'https://objkt.com' : 'https://ghostnet.objkt.com'; const normalized = rows .map((row) => ({ tokenId: Number.parseInt(row?.token?.tokenId ?? row?.token?.token_id, 10), name: row?.token?.metadata?.name || row?.token?.metadata?.symbol || `#${row?.token?.tokenId}`, balance: Number.parseInt(row?.balance ?? '0', 10), lastTime: row?.lastTime || null, })) .filter((row) => Number.isInteger(row.tokenId)) .sort((a, b) => a.tokenId - b.tokenId);
for (const token of normalized) { console.log(` [${token.tokenId}] ${token.name} (balance: ${token.balance})`); console.log(` 🔗 ${objktBase}/tokens/${contractAddress}/${token.tokenId}`); } console.log('');
return normalized;}
async function listTokenForSale(tokenReference, priceInXTZ, options = {}) { const network = options.network || 'mainnet'; const apply = options.apply === true; const referralBonusBpsRaw = Number.parseInt(options.referralBonusBps ?? 500, 10); const referralBonusBps = Number.isFinite(referralBonusBpsRaw) ? Math.max(0, Math.min(10000, referralBonusBpsRaw)) : 500; const startTime = options.startTime || null; const expiryTime = options.expiryTime || null; const replaceExisting = options.replaceExisting === true;
const { tezos, credentials, config } = await createTezosClient(network); const contractAddress = loadContractAddress(network); const tokenId = await resolveTokenIdFromReference(tokenReference, { contractAddress, network }); const priceMutez = parsePriceToMutez(priceInXTZ); const priceXTZ = (priceMutez / 1_000_000).toFixed(6);
await assertWalletOwnsToken(contractAddress, tokenId, credentials.address, network);
const token = await fetchTokenFromTzkt(contractAddress, tokenId, network); if (!token) { throw new Error(`Token #${tokenId} not found on ${network}.`); }
const tokenName = token?.metadata?.name || token?.metadata?.symbol || `#${tokenId}`; const shares = normalizeShareMap(token?.metadata?.royalties?.shares); if (Object.keys(shares).length === 0) { shares[credentials.address] = '1000'; }
const marketplaceContract = await resolveObjktMarketplaceContract({ network, keepsContract: contractAddress, explicitContract: options.marketplaceContract || null, });
const existingListing = await loadActiveListingForToken(contractAddress, tokenId, credentials.address) .catch(() => null);
if (existingListing && !replaceExisting) { const existingPrice = Number(existingListing.price_xtz || 0) / 1_000_000; throw new Error( `Token #${tokenId} already has an active listing (${existingPrice} XTZ). Use --replace to update it.` ); }
const askPayload = { token: { address: contractAddress, token_id: tokenId.toString(), }, currency: { tez: {}, }, amount: priceMutez.toString(), editions: '1', shares, start_time: startTime, expiry_time: expiryTime, referral_bonus: referralBonusBps.toString(), condition: null, };
const objktBase = network === 'mainnet' ? 'https://objkt.com' : 'https://ghostnet.objkt.com'; const tokenUrl = `${objktBase}/tokens/${contractAddress}/${tokenId}`; const existingAskOnChainId = Number.parseInt(String(existingListing?.bigmap_key), 10); const existingAskDisplayId = Number.isInteger(existingAskOnChainId) ? existingAskOnChainId : existingListing?.id;
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🏷️ List Token For Sale ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
console.log(`📡 Network: ${config.name}`); console.log(`📍 Keeps: ${contractAddress}`); console.log(`🛒 Marketplace: ${marketplaceContract}`); console.log(`🎨 Token: [${tokenId}] ${tokenName}`); console.log(`💵 Ask Price: ${priceXTZ} XTZ (${priceMutez} mutez)`); console.log(`👤 Seller: ${credentials.address}`); console.log(`🔗 View: ${tokenUrl}`); console.log(`📈 Shares: ${JSON.stringify(shares)}`); if (existingListing) { console.log( `♻️ Existing ask: #${existingAskDisplayId} (${(Number(existingListing.price_xtz || 0) / 1_000_000).toFixed(6)} XTZ)` ); } console.log('');
if (!apply) { console.log('⚠️ DRY RUN: no transaction sent. Add --yes to list on chain.\n'); return { tokenId, tokenName, priceMutez, priceXTZ: Number(priceXTZ), contractAddress, marketplaceContract, dryRun: true, replaced: Boolean(existingListing), }; }
const tokenContract = await tezos.contract.at(contractAddress); const marketContract = await tezos.contract.at(marketplaceContract);
const askMethod = marketContract.methodsObject.ask(askPayload); const retractMethod = existingListing ? pickMethod(marketContract, 'retract_ask')(Number(existingAskDisplayId)) : null;
let op; let mode = 'ask-only';
if (retractMethod) { mode = 'retract+ask'; op = await tezos.contract .batch() .withContractCall(retractMethod) .withContractCall(askMethod) .send(); } else { const addOperatorMethod = pickMethod(tokenContract, 'update_operators')([ { add_operator: { owner: credentials.address, operator: marketplaceContract, token_id: tokenId, }, }, ]);
try { mode = 'add_operator+ask'; op = await tezos.contract .batch() .withContractCall(addOperatorMethod) .withContractCall(askMethod) .send(); } catch (error) { const message = String(error?.message || error); if (/FA2_OPERATOR_ALREADY_EXISTS|operator/i.test(message)) { mode = 'ask-only'; op = await tezos.contract .batch() .withContractCall(askMethod) .send(); } else { throw error; } } }
console.log(` Transaction: ${op.hash}`); console.log(' Waiting for confirmation...'); await op.confirmation(1);
console.log('\n✅ Listed successfully'); console.log(` Mode: ${mode}`); console.log(` Explorer: ${config.explorer}/${op.hash}`); console.log(` Objkt: ${tokenUrl}\n`);
return { tokenId, tokenName, priceMutez, priceXTZ: Number(priceXTZ), contractAddress, marketplaceContract, hash: op.hash, mode, replaced: Boolean(existingListing), };}
async function listBatchForSale(items = [], options = {}) { if (!Array.isArray(items) || items.length === 0) { throw new Error('No items to list. Use format: <token|piece>=<priceXTZ>'); }
const results = []; for (const item of items) { const [tokenReference, priceInXTZ] = item.split('='); if (!tokenReference || !priceInXTZ) { throw new Error(`Invalid batch item "${item}". Expected "<token|piece>=<priceXTZ>".`); }
const result = await listTokenForSale(tokenReference, priceInXTZ, options); results.push(result); }
return results;}
// Cancel an active Objkt listing. Callable by the original ask creator regardless of// whether the token has since been transferred away — useful for retracting stale asks.async function cancelListing(tokenReference, options = {}) { const { network = 'mainnet', apply = false } = options; const { tezos, credentials, config } = await createTezosClient(network); const contractAddress = loadContractAddress(network);
const tokenId = await resolveTokenIdFromReference(tokenReference, { contractAddress, network });
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ ♻️ Cancel Listing ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${contractAddress}`); console.log(`🎨 Token: #${tokenId} (${tokenReference})`); console.log(`👤 Seller: ${credentials.address}`);
const listing = await loadActiveListingForToken(contractAddress, tokenId, credentials.address); if (!listing) { console.log(`\nℹ️ No active listing found for token #${tokenId} by ${credentials.address}.\n`); return { hash: null, retracted: null }; }
const askId = Number.parseInt(String(listing.bigmap_key), 10) || listing.id; const askPrice = (Number(listing.price_xtz || 0) / 1_000_000).toFixed(6); const marketplaceContract = listing.marketplace_contract || OBJKT_MARKETPLACE_FALLBACK[network];
console.log(`💰 Price: ${askPrice} XTZ`); console.log(`🆔 Ask: #${askId}`); console.log(`🏪 Market: ${marketplaceContract}`);
try { await assertWalletOwnsToken(contractAddress, tokenId, credentials.address, network); console.log(`📦 Status: active (seller still holds token)`); } catch { console.log(`📦 Status: DEAD — seller no longer owns token (cleanup retract)`); }
if (!apply) { console.log('\n🟡 DRY RUN — pass --yes to send transaction.\n'); return { hash: null, retracted: askId, dryRun: true }; }
const marketContract = await tezos.contract.at(marketplaceContract); const op = await pickMethod(marketContract, 'retract_ask')(Number(askId)).send(); console.log(`\n ⏳ Operation: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1); console.log('\n✅ Listing retracted!'); console.log(` 🔗 ${config.explorer}/${op.hash}\n`); return { hash: op.hash, retracted: askId };}
async function cancelBatch(tokenReferences = [], options = {}) { const { network = 'mainnet', apply = false } = options; const results = []; console.log(`\n🧹 Cancel batch: ${tokenReferences.length} token(s) on ${network} (apply=${apply})\n`); for (const ref of tokenReferences) { try { const r = await cancelListing(ref, { network, apply }); results.push({ ref, ...r }); } catch (e) { console.error(`❌ ${ref}: ${e.message}`); results.push({ ref, error: e.message }); } } return results;}
async function acceptOffer(offerIdInput, options = {}) { const network = options.network || 'mainnet'; const apply = options.apply === true; const minPriceMutez = options.minPriceMutez != null ? Number.parseInt(String(options.minPriceMutez), 10) : null;
const { tezos, credentials, config } = await createTezosClient(network); const contractAddress = loadContractAddress(network); const offerId = Number.parseInt(String(offerIdInput), 10);
if (!Number.isInteger(offerId) || offerId < 0) { throw new Error(`Invalid offer id "${offerIdInput}".`); }
const offer = await loadActiveOfferById(offerId); if (!offer) { throw new Error(`Offer #${offerId} is not active.`); }
const tokenId = Number.parseInt(String(offer?.token?.token_id), 10); const tokenName = offer?.token?.name || `#${offer?.token?.token_id ?? '?'}`; const offerRowId = Number.parseInt(String(offer?.id), 10); const offerOnChainId = Number.parseInt(String(offer?.bigmap_key), 10); const chainOfferId = Number.isInteger(offerOnChainId) ? offerOnChainId : offerId; const faContract = offer?.token?.fa_contract; if (!isKt1Address(faContract) || faContract !== contractAddress) { throw new Error( `Offer #${offerId} belongs to ${faContract || 'unknown contract'}, not current keeps contract ${contractAddress}.` ); }
const bidMutez = Number.parseInt(String(offer?.price_xtz || 0), 10); if (!Number.isInteger(bidMutez) || bidMutez <= 0) { throw new Error(`Offer #${offerId} has invalid bid amount.`); }
if (Number.isInteger(minPriceMutez) && bidMutez <= minPriceMutez) { throw new Error( `Offer #${offerId} bid ${(bidMutez / 1_000_000).toFixed(6)} XTZ is not above threshold ${(minPriceMutez / 1_000_000).toFixed(6)} XTZ.` ); }
await assertWalletOwnsToken(contractAddress, tokenId, credentials.address, network);
const marketplaceContract = options.marketplaceContract || offer?.marketplace_contract || await resolveObjktMarketplaceContract({ network, keepsContract: contractAddress, explicitContract: null, });
const bidXTZ = (bidMutez / 1_000_000).toFixed(6); const objktBase = network === 'mainnet' ? 'https://objkt.com' : 'https://ghostnet.objkt.com'; const tokenUrl = `${objktBase}/tokens/${contractAddress}/${tokenId}`;
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ ✅ Accept Offer ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${config.name}`); console.log(`📍 Keeps: ${contractAddress}`); console.log(`🛒 Marketplace: ${marketplaceContract}`); console.log(`🎨 Token: [${tokenId}] ${tokenName}`); console.log(`🧾 Offer ID: ${chainOfferId}`); if (Number.isInteger(offerRowId) && offerRowId !== chainOfferId) { console.log(`🗂️ Offer Row ID: ${offerRowId}`); } console.log(`💵 Bid: ${bidXTZ} XTZ (${bidMutez} mutez)`); console.log(`👤 Buyer: ${offer?.buyer_address || 'unknown'}`); console.log(`👤 Seller: ${credentials.address}`); console.log(`🔗 View: ${tokenUrl}\n`);
if (!apply) { console.log('⚠️ DRY RUN: no transaction sent. Add --yes to accept on chain.\n'); return { offerId: chainOfferId, offerRowId: Number.isInteger(offerRowId) ? offerRowId : null, tokenId, tokenName, bidMutez, bidXTZ: Number(bidXTZ), buyerAddress: offer?.buyer_address || null, marketplaceContract, contractAddress, dryRun: true, }; }
const tokenContract = await tezos.contract.at(contractAddress); const marketContract = await tezos.contract.at(marketplaceContract);
const addOperatorMethod = pickMethod(tokenContract, 'update_operators')([ { add_operator: { owner: credentials.address, operator: marketplaceContract, token_id: tokenId, }, }, ]);
const fulfillMethod = marketContract.methodsObject.fulfill_offer({ offer_id: chainOfferId, token_id: tokenId, condition_extra: null, });
let op; let mode = 'add_operator+fulfill_offer';
try { op = await tezos.contract .batch() .withContractCall(addOperatorMethod) .withContractCall(fulfillMethod) .send(); } catch (error) { const message = String(error?.message || error); if (/FA2_OPERATOR_ALREADY_EXISTS|operator/i.test(message)) { mode = 'fulfill_offer-only'; op = await tezos.contract .batch() .withContractCall(fulfillMethod) .send(); } else { throw error; } }
console.log(` Transaction: ${op.hash}`); console.log(' Waiting for confirmation...'); await op.confirmation(1);
console.log('\n✅ Offer accepted'); console.log(` Mode: ${mode}`); console.log(` Explorer: ${config.explorer}/${op.hash}`); console.log(` Objkt: ${tokenUrl}\n`);
return { offerId: chainOfferId, offerRowId: Number.isInteger(offerRowId) ? offerRowId : null, tokenId, tokenName, bidMutez, bidXTZ: Number(bidXTZ), buyerAddress: offer?.buyer_address || null, marketplaceContract, contractAddress, hash: op.hash, mode, };}
async function acceptOffersAboveThreshold(items = [], options = {}) { if (!Array.isArray(items) || items.length === 0) { throw new Error('No items provided. Use format: <token|piece>=<minimumXTZ>'); }
const network = options.network || 'mainnet'; const apply = options.apply === true; const contractAddress = loadContractAddress(network);
const checks = []; for (const item of items) { const [tokenReference, minXTZ] = item.split('='); if (!tokenReference || !minXTZ) { throw new Error(`Invalid item "${item}". Expected "<token|piece>=<minimumXTZ>".`); }
const tokenId = await resolveTokenIdFromReference(tokenReference, { contractAddress, network }); const minPriceMutez = parsePriceToMutez(minXTZ); const bestOffer = await loadBestActiveOfferForToken(contractAddress, tokenId);
checks.push({ tokenReference, tokenId, minPriceMutez, minXTZ: minPriceMutez / 1_000_000, offer: bestOffer, qualifies: Number(bestOffer?.price_xtz || 0) > minPriceMutez, }); }
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🤝 Accept Offers Above Threshold ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${network}`); console.log(`📍 Keeps: ${contractAddress}\n`);
for (const row of checks) { if (!row.offer) { console.log(` [${row.tokenId}] no active offer (threshold: ${row.minXTZ.toFixed(6)} XTZ)`); continue; }
const bidMutez = Number(row.offer.price_xtz || 0); const bidXTZ = bidMutez / 1_000_000; const pass = row.qualifies ? '✅' : '❌'; const onChainOfferId = Number.parseInt(String(row.offer.bigmap_key), 10); const displayOfferId = Number.isInteger(onChainOfferId) ? onChainOfferId : row.offer.id; console.log( ` [${row.tokenId}] ${row.offer?.token?.name || '#'} offer #${displayOfferId} bid ${bidXTZ.toFixed(6)} XTZ vs threshold ${row.minXTZ.toFixed(6)} XTZ ${pass}` ); } console.log('');
const qualifying = checks.filter((row) => row.offer && row.qualifies); if (qualifying.length === 0) { console.log('No qualifying offers above thresholds.\n'); return []; }
if (!apply) { console.log('⚠️ DRY RUN: no transaction sent. Add --yes to accept qualifying offers.\n'); return qualifying.map((row) => ({ tokenId: row.tokenId, tokenName: row.offer?.token?.name || null, offerId: Number.parseInt(String(row.offer?.bigmap_key), 10) || Number(row.offer.id), offerRowId: Number(row.offer.id), bidMutez: Number(row.offer.price_xtz || 0), bidXTZ: Number(row.offer.price_xtz || 0) / 1_000_000, thresholdXTZ: row.minXTZ, buyerAddress: row.offer?.buyer_address || null, dryRun: true, })); }
const results = []; for (const row of qualifying) { const onChainOfferId = Number.parseInt(String(row.offer?.bigmap_key), 10); const offerIdentifier = Number.isInteger(onChainOfferId) ? onChainOfferId : row.offer.id; const result = await acceptOffer(offerIdentifier, { network, apply: true, minPriceMutez: row.minPriceMutez, marketplaceContract: options.marketplaceContract || row.offer?.marketplace_contract || null, }); results.push(result); }
return results;}
// ============================================================================// Buy (fulfill_ask)// ============================================================================
async function loadActiveAskById(askId) { const numericId = Number.parseInt(String(askId), 10); if (!Number.isInteger(numericId) || numericId < 0) { throw new Error(`Invalid ask id "${askId}".`); }
const data = await objktGraphQL( ` query { listing_active( where:{ _or:[ {id:{_eq:${numericId}}}, {bigmap_key:{_eq:${numericId}}} ] } order_by:{timestamp:desc} limit:5 ) { id bigmap_key price_xtz seller_address marketplace_contract marketplace { name } amount_left timestamp token { token_id name fa_contract } } } ` );
const rows = Array.isArray(data?.listing_active) ? data.listing_active : []; if (rows.length === 0) return null;
return rows.find((row) => Number.parseInt(String(row?.id), 10) === numericId) || rows.find((row) => Number.parseInt(String(row?.bigmap_key), 10) === numericId) || rows[0];}
async function buyToken(askIdInput, options = {}) { const network = options.network || 'mainnet'; const apply = options.apply === true;
const { tezos, credentials, config } = await createTezosClient(network); const contractAddress = loadContractAddress(network); const askId = Number.parseInt(String(askIdInput), 10);
if (!Number.isInteger(askId) || askId < 0) { throw new Error(`Invalid ask id "${askIdInput}".`); }
// Try objkt GraphQL first, fall back to TzKT bigmap lookup let listing = await loadActiveAskById(askId); let priceMutez, tokenId, tokenName, sellerAddress, marketplaceContract, faContract;
if (listing) { priceMutez = Number.parseInt(String(listing.price_xtz || 0), 10); tokenId = Number.parseInt(String(listing.token?.token_id), 10); tokenName = listing.token?.name || `#${tokenId}`; sellerAddress = listing.seller_address; marketplaceContract = listing.marketplace_contract; faContract = listing.token?.fa_contract; } else { // Fallback: read directly from TzKT bigmap const bigmapId = 684371; // objktcom marketplace v6.2 asks bigmap const resp = await fetch(`https://api.tzkt.io/v1/bigmaps/${bigmapId}/keys/${askId}`); if (!resp.ok) throw new Error(`Ask #${askId} not found (TzKT ${resp.status}).`); const entry = await resp.json(); if (!entry?.active) throw new Error(`Ask #${askId} is no longer active.`); const val = entry.value; priceMutez = Number.parseInt(String(val?.amount || 0), 10); tokenId = Number.parseInt(String(val?.token?.token_id), 10); tokenName = `#${tokenId}`; sellerAddress = val?.creator; faContract = val?.token?.address; marketplaceContract = 'KT1SwbTqhSKF6Pdokiu1K4Fpi17ahPPzmt1X'; }
if (!Number.isInteger(priceMutez) || priceMutez <= 0) { throw new Error(`Ask #${askId} has invalid price.`); }
if (faContract && faContract !== contractAddress) { // Allow buying from any FA2, but warn if not the current keeps contract console.log(`⚠️ Token is from ${faContract}, not current keeps contract ${contractAddress}.`); }
const priceXTZ = (priceMutez / 1_000_000).toFixed(6); const balance = await tezos.tz.getBalance(credentials.address); const balanceXTZ = balance.toNumber() / 1_000_000;
if (balanceXTZ < priceMutez / 1_000_000) { throw new Error( `Insufficient balance: ${balanceXTZ.toFixed(6)} XTZ available, need ${priceXTZ} XTZ.` ); }
const objktBase = network === 'mainnet' ? 'https://objkt.com' : 'https://ghostnet.objkt.com'; const tokenUrl = faContract ? `${objktBase}/tokens/${faContract}/${tokenId}` : `${objktBase}/tokens/${contractAddress}/${tokenId}`;
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🛒 Buy Token (fulfill_ask) ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(`📡 Network: ${config.name}`); console.log(`📍 Contract: ${faContract || contractAddress}`); console.log(`🛒 Marketplace: ${marketplaceContract}`); console.log(`🎨 Token: [${tokenId}] ${tokenName}`); console.log(`🧾 Ask ID: ${askId}`); console.log(`💵 Price: ${priceXTZ} XTZ (${priceMutez} mutez)`); console.log(`👤 Seller: ${sellerAddress || 'unknown'}`); console.log(`👤 Buyer: ${credentials.address}`); console.log(`💰 Balance: ${balanceXTZ.toFixed(6)} XTZ`); console.log(`🔗 View: ${tokenUrl}\n`);
if (!apply) { console.log('⚠️ DRY RUN: no transaction sent. Add --yes to buy on chain.\n'); return { askId, tokenId, tokenName, priceMutez, priceXTZ: Number(priceXTZ), sellerAddress, marketplaceContract, dryRun: true, }; }
const marketContract = await tezos.contract.at(marketplaceContract);
const op = await marketContract.methodsObject.fulfill_ask({ ask_id: askId, amount: 1, // editions to buy proxy_for: null, condition_extra: null, referrers: new MichelsonMap(), }).send({ amount: priceMutez, mutez: true });
console.log(` ⏳ Transaction: ${op.hash}`); console.log(' ⏳ Waiting for confirmation...'); await op.confirmation(1);
console.log('\n✅ Purchase complete!'); console.log(` 🎨 Token: [${tokenId}] ${tokenName}`); console.log(` 💵 Paid: ${priceXTZ} XTZ`); console.log(` 🔗 Explorer: ${config.explorer}/${op.hash}`); console.log(` 🔗 Objkt: ${tokenUrl}\n`);
return { askId, tokenId, tokenName, priceMutez, priceXTZ: Number(priceXTZ), sellerAddress, marketplaceContract, hash: op.hash, };}
async function showMarketSnapshot(network = 'mainnet', options = {}) { if (network !== 'mainnet') { throw new Error('Market snapshot currently supports mainnet only.'); }
const contractAddress = loadContractAddress(network); const listingsLimit = Number.isFinite(Number(options.listingsLimit)) ? Math.max(1, Number(options.listingsLimit)) : 20; const salesLimit = Number.isFinite(Number(options.salesLimit)) ? Math.max(1, Number(options.salesLimit)) : 10;
const collectionData = await objktGraphQL( ` query($contract:String!, $limit:Int!) { fa(where:{contract:{_eq:$contract}}) { contract name items owners floor_price volume_24h volume_total } listing_active( where:{fa_contract:{_eq:$contract}} order_by:{price_xtz:asc} limit:$limit ) { id bigmap_key price_xtz seller_address token { token_id name } marketplace { name } timestamp } offer_active( where:{fa_contract:{_eq:$contract}} order_by:{price_xtz:desc} limit:$limit ) { id bigmap_key price_xtz buyer_address token { token_id name } marketplace { name } timestamp } } `, { contract: contractAddress, limit: listingsLimit } );
let salesData = { listing_sale: [] }; let salesLoadError = null; try { salesData = await objktGraphQL( ` query($contract:String!, $limit:Int!) { listing_sale( where:{token:{fa_contract:{_eq:$contract}}} order_by:{timestamp:desc} limit:$limit ) { timestamp price_xtz buyer_address seller_address token { token_id name } marketplace { name } } } `, { contract: contractAddress, limit: salesLimit } ); } catch (error) { salesLoadError = error; }
const collection = collectionData?.fa?.[0] || null; const listings = Array.isArray(collectionData?.listing_active) ? collectionData.listing_active : []; const offers = Array.isArray(collectionData?.offer_active) ? collectionData.offer_active : []; const sales = Array.isArray(salesData?.listing_sale) ? salesData.listing_sale : [];
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 📈 Objkt Market Snapshot ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
console.log(`📍 Contract: ${contractAddress}`); console.log(`🎨 Collection: ${collection?.name || 'Unknown'}`); console.log(`🧱 Items: ${collection?.items ?? 'n/a'} | 👥 Owners: ${collection?.owners ?? 'n/a'}`); console.log(`🏷️ Floor: ${collection?.floor_price != null ? (Number(collection.floor_price) / 1_000_000).toFixed(6) : 'n/a'} XTZ`); console.log(`📊 Volume 24h: ${collection?.volume_24h != null ? (Number(collection.volume_24h) / 1_000_000).toFixed(6) : 'n/a'} XTZ`); console.log(`📚 Volume total: ${collection?.volume_total != null ? (Number(collection.volume_total) / 1_000_000).toFixed(6) : 'n/a'} XTZ\n`);
console.log(`🛒 Active Listings (${listings.length}):`); if (listings.length === 0) { console.log(' (none)'); } else { for (const row of listings) { const askId = Number.parseInt(String(row?.bigmap_key), 10); const displayAskId = Number.isInteger(askId) ? askId : row?.id; console.log( ` [${row?.token?.token_id}] ${row?.token?.name || '#'} ask #${displayAskId} @ ${(Number(row?.price_xtz || 0) / 1_000_000).toFixed(6)} XTZ (${row?.seller_address})` ); } } console.log('');
console.log(`🤝 Active Offers (${offers.length}):`); if (offers.length === 0) { console.log(' (none)'); } else { for (const row of offers) { const offerId = Number.parseInt(String(row?.bigmap_key), 10); const displayOfferId = Number.isInteger(offerId) ? offerId : row?.id; console.log( ` [${row?.token?.token_id}] ${row?.token?.name || '#'} offer #${displayOfferId} bid ${(Number(row?.price_xtz || 0) / 1_000_000).toFixed(6)} XTZ (${row?.buyer_address})` ); } } console.log('');
console.log(`💸 Recent Sales (${sales.length}):`); if (sales.length === 0) { if (salesLoadError) { console.log(` (unavailable: ${salesLoadError.message})`); } else { console.log(' (none)'); } } else { for (const row of sales) { console.log( ` [${row?.token?.token_id}] ${row?.token?.name || '#'} sold ${(Number(row?.price_xtz || 0) / 1_000_000).toFixed(6)} XTZ @ ${row?.timestamp}` ); } } console.log('');
return { contractAddress, collection, listings, offers, sales, };}
async function discoverContractsByManager(managerAddress, network = 'mainnet') { const apiBase = tzktApiBase(network); const url = `${apiBase}/v1/accounts/${managerAddress}/contracts?limit=200&sort.desc=creationLevel`; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to load contracts for ${managerAddress}: ${response.status}`); } const contracts = await response.json(); return contracts .map((entry) => entry?.address) .filter((address) => isKt1Address(address));}
function decodeContractMetadataBytes(contentBytes) { if (!contentBytes) return null;
const raw = typeof contentBytes === 'string' ? contentBytes : (typeof contentBytes?.toString === 'function' ? contentBytes.toString() : ''); const normalized = raw.startsWith('0x') ? raw.slice(2) : raw; if (!normalized) return null;
try { return JSON.parse(Buffer.from(normalized, 'hex').toString('utf8')); } catch { return null; }}
function buildDeprecatedCollectionMetadata(existing = {}, options = {}) { const replacementContract = options.replacementContract; const nowIso = new Date().toISOString(); const nowDate = nowIso.slice(0, 10); const priorDescription = typeof existing.description === 'string' ? existing.description.trim() : ''; const replacementText = replacementContract ? `Replacement contract: ${replacementContract}` : 'Replacement contract: not set'; const notice = options.notice || `Deprecated staging contract as of ${nowDate}. ${replacementText}`;
const deprecatedName = typeof existing.name === 'string' && existing.name.trim() ? (existing.name.includes('[DEPRECATED]') ? existing.name : `${existing.name} [DEPRECATED]`) : 'KidLisp Keeps [DEPRECATED]';
return { ...existing, name: deprecatedName, description: [priorDescription, notice].filter(Boolean).join('\n\n'), deprecated: true, deprecatedAt: nowIso, deprecatedReplacement: replacementContract || null, status: 'deprecated-staging', };}
async function fetchActiveTokenIds(contractAddress, network = 'mainnet') { const apiBase = tzktApiBase(network); const limit = 1000; let offset = 0; const tokenIds = [];
while (true) { const url = `${apiBase}/v1/contracts/${contractAddress}/bigmaps/token_metadata/keys?active=true&select=key&limit=${limit}&offset=${offset}`; const response = await fetch(url); if (!response.ok) { throw new Error(`Failed to fetch token list for ${contractAddress}: ${response.status}`); }
const keys = await response.json(); if (!Array.isArray(keys) || keys.length === 0) break;
for (const key of keys) { const tokenId = Number(key); if (Number.isInteger(tokenId) && tokenId >= 0) { tokenIds.push(tokenId); } }
if (keys.length < limit) break; offset += keys.length; }
return [...new Set(tokenIds)].sort((a, b) => a - b);}
async function deprecateStagingContracts(options = {}) { const network = options.network || 'mainnet'; const apply = options.apply === true; const providedAddresses = Array.isArray(options.addresses) ? options.addresses.filter((address) => isKt1Address(address)) : []; const replacementContract = isKt1Address(options.replacementContract) ? options.replacementContract : null; const burnLimitRaw = Number(options.burnLimit); const burnLimit = Number.isFinite(burnLimitRaw) && burnLimitRaw > 0 ? Math.floor(burnLimitRaw) : Number.POSITIVE_INFINITY;
const { tezos, credentials, config } = await createTezosClient(network);
let contractAddresses = providedAddresses; if (contractAddresses.length === 0) { contractAddresses = await discoverContractsByManager(credentials.address, network); }
if (contractAddresses.length === 0) { throw new Error(`No contracts found for ${credentials.address} on ${network}`); }
console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 🧹 Deprecating Staging Contracts ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n');
console.log(`📡 Network: ${config.name}`); console.log(`👤 Wallet: ${credentials.address}`); console.log(`📦 Contracts: ${contractAddresses.length}`); if (!apply) { console.log('⚠️ DRY RUN (no transactions will be sent). Use --yes to apply.\n'); } else { console.log('⚠️ APPLY MODE (transactions will be sent).\n'); }
const summary = [];
for (const contractAddress of contractAddresses) { const row = { contractAddress, paused: null, metadataLocked: null, activeTokenCount: 0, burned: 0, operations: [], skipped: false, errors: [], };
summary.push(row); console.log(`\n──────────────────────────────────────────────────────────────`); console.log(`📍 ${contractAddress}`); console.log(`🔗 ${config.explorer}/${contractAddress}`);
try { const contract = await tezos.contract.at(contractAddress); const storage = await contract.storage();
const looksLikeKeeps = storage && storage.metadata && storage.token_metadata && storage.keep_fee !== undefined && storage.default_royalty_bps !== undefined;
if (!looksLikeKeeps) { row.skipped = true; console.log(' ⏭️ Skipping (does not match Keeps storage shape)'); continue; }
row.paused = storage.paused === true; row.metadataLocked = storage.contract_metadata_locked === true; const nextTokenId = Number(storage.next_token_id?.toNumber?.() ?? storage.next_token_id ?? 0); const tokenIds = await fetchActiveTokenIds(contractAddress, network); row.activeTokenCount = tokenIds.length;
console.log(` • Paused: ${row.paused}`); console.log(` • Metadata locked: ${row.metadataLocked}`); console.log(` • Next token id: ${nextTokenId}`); console.log(` • Active tokens: ${row.activeTokenCount}`);
if (!apply) { continue; }
if (!row.metadataLocked) { try { const existingContent = await storage.metadata.get('content'); const existingMetadata = decodeContractMetadataBytes(existingContent) || {}; const deprecatedMetadata = buildDeprecatedCollectionMetadata(existingMetadata, { replacementContract, }); const metadataBytes = stringToBytes(JSON.stringify(deprecatedMetadata)); const metadataOp = await pickMethod(contract, 'set_contract_metadata')([ { key: 'content', value: `0x${metadataBytes}` }, ]).send(); console.log(` ⏳ Deprecation metadata op: ${metadataOp.hash}`); await metadataOp.confirmation(1); row.operations.push({ step: 'set_contract_metadata', hash: metadataOp.hash }); console.log(' ✅ Collection metadata marked deprecated'); } catch (error) { row.errors.push(`set_contract_metadata: ${error.message}`); console.log(` ⚠️ Could not set deprecation metadata: ${error.message}`); } } else { console.log(' ℹ️ Metadata already locked; cannot write deprecation notice'); }
if (!row.paused) { try { const pauseOp = await contract.methodsObject.pause().send(); console.log(` ⏳ Pause op: ${pauseOp.hash}`); await pauseOp.confirmation(1); row.operations.push({ step: 'pause', hash: pauseOp.hash }); row.paused = true; console.log(' ✅ Contract paused'); } catch (error) { row.errors.push(`pause: ${error.message}`); console.log(` ⚠️ Could not pause: ${error.message}`); } } else { console.log(' ℹ️ Already paused'); }
const burnQueue = Number.isFinite(burnLimit) ? tokenIds.slice(0, burnLimit) : tokenIds;
if (burnQueue.length > 0) { console.log(` 🔥 Burning ${burnQueue.length} token(s)...`); }
for (const tokenId of burnQueue) { try { const burnOp = await pickMethod(contract, 'burn_keep')(tokenId).send(); console.log(` ⏳ burn #${tokenId}: ${burnOp.hash}`); await burnOp.confirmation(1); row.operations.push({ step: 'burn_keep', tokenId, hash: burnOp.hash }); row.burned += 1; } catch (error) { row.errors.push(`burn_keep(${tokenId}): ${error.message}`); console.log(` ⚠️ burn #${tokenId} failed: ${error.message}`); } }
if (!row.metadataLocked) { try { const lockOp = await pickMethod(contract, 'lock_contract_metadata')().send(); console.log(` ⏳ Lock metadata op: ${lockOp.hash}`); await lockOp.confirmation(1); row.operations.push({ step: 'lock_contract_metadata', hash: lockOp.hash }); row.metadataLocked = true; console.log(' ✅ Collection metadata locked'); } catch (error) { row.errors.push(`lock_contract_metadata: ${error.message}`); console.log(` ⚠️ Could not lock metadata: ${error.message}`); } } else { console.log(' ℹ️ Metadata already locked'); } } catch (error) { row.errors.push(error.message); console.log(` ❌ Contract processing failed: ${error.message}`); } }
console.log('\n══════════════════════════════════════════════════════════════'); console.log('📋 Staging Deprecation Summary'); for (const row of summary) { console.log(` ${row.contractAddress}`); if (row.skipped) { console.log(' - skipped'); continue; } console.log(` - paused: ${row.paused}`); console.log(` - metadataLocked: ${row.metadataLocked}`); console.log(` - activeTokens(before): ${row.activeTokenCount}`); console.log(` - burned: ${row.burned}`); console.log(` - ops: ${row.operations.length}`); if (row.errors.length > 0) { console.log(` - errors: ${row.errors.length}`); row.errors.forEach((message) => console.log(` • ${message}`)); } } console.log('');
return { network, wallet: credentials.address, apply, contracts: summary, };}
// ============================================================================// CLI Interface// ============================================================================
async function main() { const rawArgs = process.argv.slice(2); // Separate flags from positional arguments const flags = rawArgs.filter(a => a.startsWith('--')); const args = rawArgs.filter(a => !a.startsWith('--')); const command = args[0]; const contractFlag = flags.find(f => f.startsWith('--contract=') || f.startsWith('--profile=')); const contractProfile = contractFlag ? contractFlag.split('=').slice(1).join('=').trim() : 'v9'; // Parse --wallet flag const walletFlag = flags.find(f => f.startsWith('--wallet=')); if (walletFlag) { const wallet = walletFlag.split('=')[1]; if (['keeps', 'kidlisp', 'aesthetic', 'staging'].includes(wallet)) { setWallet(wallet); console.log(`🔑 Using wallet: ${wallet}\n`); } else { console.error(`❌ Unknown wallet: ${wallet}. Use: keeps, kidlisp, aesthetic, or staging`); process.exit(1); } } // Helper to get network from args (defaults to mainnet) const getNetwork = (argIndex) => { const val = args[argIndex]; if (!val || val.startsWith('--')) return 'mainnet'; return val; }; try { switch (command) { case 'deploy': await deployContract(getNetwork(1), { contractProfile }); break;
case 'sync-secrets': await syncCurrentContractToSecrets(getNetwork(1), { contractProfile }); break; case 'status': await getContractStatus(getNetwork(1)); break; case 'balance': await getBalance(getNetwork(1)); break;
case 'wallets': await getAllWalletBalances(); break;
case 'tokens': { const limitFlag = flags.find(f => f.startsWith('--limit=')); const limit = limitFlag ? Number.parseInt(limitFlag.split('=')[1], 10) : undefined; await listOwnedTokens(getNetwork(1), { limit }); break; }
case 'market': { const listingsLimitFlag = flags.find(f => f.startsWith('--listings=')); const salesLimitFlag = flags.find(f => f.startsWith('--sales=')); const listingsLimit = listingsLimitFlag ? Number.parseInt(listingsLimitFlag.split('=')[1], 10) : undefined; const salesLimit = salesLimitFlag ? Number.parseInt(salesLimitFlag.split('=')[1], 10) : undefined; await showMarketSnapshot(getNetwork(1), { listingsLimit, salesLimit }); break; }
case 'sell': { if (!args[1] || !args[2]) { console.error('Usage: node keeps.mjs sell <token_id|$piece> <price_xtz> [network] [--marketplace=<KT1...>] [--replace] [--yes]'); process.exit(1); }
const marketplaceFlag = flags.find(f => f.startsWith('--marketplace=')); const referralFlag = flags.find(f => f.startsWith('--referral-bps=')); const startFlag = flags.find(f => f.startsWith('--start=')); const expiryFlag = flags.find(f => f.startsWith('--expiry=')); const marketplaceContract = marketplaceFlag ? marketplaceFlag.split('=').slice(1).join('=').trim() : null; const referralBonusBps = referralFlag ? Number.parseInt(referralFlag.split('=')[1], 10) : undefined; const startTime = parseOptionalIsoTimestamp(startFlag ? startFlag.split('=').slice(1).join('=') : null, 'start'); const expiryTime = parseOptionalIsoTimestamp(expiryFlag ? expiryFlag.split('=').slice(1).join('=') : null, 'expiry');
await listTokenForSale(args[1], args[2], { network: getNetwork(3), marketplaceContract, referralBonusBps, startTime, expiryTime, replaceExisting: flags.includes('--replace'), apply: flags.includes('--yes') || flags.includes('--apply'), }); break; }
case 'sell:batch': { const inputItems = args.slice(1); if (inputItems.length === 0) { console.error('Usage: node keeps.mjs sell:batch <token|piece=price_xtz> [...] [network] [--marketplace=<KT1...>] [--replace] [--yes]'); process.exit(1); }
let network = 'mainnet'; if (['mainnet', 'ghostnet'].includes(inputItems[inputItems.length - 1])) { network = inputItems.pop(); }
if (inputItems.length === 0) { console.error('❌ No batch items supplied. Example: node keeps.mjs sell:batch \'$faim=5.5\' \'$tezz=5.8\' \'$bip=6\' --yes'); process.exit(1); }
const marketplaceFlag = flags.find(f => f.startsWith('--marketplace=')); const referralFlag = flags.find(f => f.startsWith('--referral-bps=')); const startFlag = flags.find(f => f.startsWith('--start=')); const expiryFlag = flags.find(f => f.startsWith('--expiry=')); const marketplaceContract = marketplaceFlag ? marketplaceFlag.split('=').slice(1).join('=').trim() : null; const referralBonusBps = referralFlag ? Number.parseInt(referralFlag.split('=')[1], 10) : undefined; const startTime = parseOptionalIsoTimestamp(startFlag ? startFlag.split('=').slice(1).join('=') : null, 'start'); const expiryTime = parseOptionalIsoTimestamp(expiryFlag ? expiryFlag.split('=').slice(1).join('=') : null, 'expiry');
await listBatchForSale(inputItems, { network, marketplaceContract, referralBonusBps, startTime, expiryTime, replaceExisting: flags.includes('--replace'), apply: flags.includes('--yes') || flags.includes('--apply'), }); break; }
case 'accept': { if (!args[1]) { console.error('Usage: node keeps.mjs accept <offer_id> [network] [--marketplace=<KT1...>] [--min=<xtz>] [--yes]'); process.exit(1); }
const marketplaceFlag = flags.find(f => f.startsWith('--marketplace=')); const minFlag = flags.find(f => f.startsWith('--min=')); const marketplaceContract = marketplaceFlag ? marketplaceFlag.split('=').slice(1).join('=').trim() : null;
let minPriceMutez = null; if (minFlag) { const raw = minFlag.split('=').slice(1).join('=').trim(); const minXTZ = Number.parseFloat(raw); if (!Number.isFinite(minXTZ) || minXTZ < 0) { console.error(`❌ Invalid --min value "${raw}". Expected a non-negative XTZ amount.`); process.exit(1); } minPriceMutez = Math.round(minXTZ * 1_000_000); }
await acceptOffer(args[1], { network: getNetwork(2), marketplaceContract, minPriceMutez, apply: flags.includes('--yes') || flags.includes('--apply'), }); break; }
case 'accept:auto': { const inputItems = args.slice(1); if (inputItems.length === 0) { console.error('Usage: node keeps.mjs accept:auto <token|piece=min_xtz> [...] [network] [--marketplace=<KT1...>] [--yes]'); process.exit(1); }
let network = 'mainnet'; if (['mainnet', 'ghostnet'].includes(inputItems[inputItems.length - 1])) { network = inputItems.pop(); }
if (inputItems.length === 0) { console.error('❌ No accept:auto items supplied. Example: node keeps.mjs accept:auto \'$faim=8\' \'$tezz=9.5\' --yes'); process.exit(1); }
const marketplaceFlag = flags.find(f => f.startsWith('--marketplace=')); const marketplaceContract = marketplaceFlag ? marketplaceFlag.split('=').slice(1).join('=').trim() : null;
await acceptOffersAboveThreshold(inputItems, { network, marketplaceContract, apply: flags.includes('--yes') || flags.includes('--apply'), }); break; } case 'buy': { if (!args[1]) { console.error('Usage: node keeps.mjs buy <ask_id> [network] [--marketplace=<KT1...>] [--yes]'); console.error(''); console.error('Fulfill an active Objkt marketplace listing (ask) to buy a token.'); console.error(''); console.error('Examples:'); console.error(' node keeps.mjs buy 12589569 --wallet=aesthetic # Dry run'); console.error(' node keeps.mjs buy 12589569 --wallet=aesthetic --yes # Live purchase'); process.exit(1); }
const buyMarketFlag = flags.find(f => f.startsWith('--marketplace=')); const buyMarketplace = buyMarketFlag ? buyMarketFlag.split('=').slice(1).join('=').trim() : null;
await buyToken(args[1], { network: getNetwork(2), marketplaceContract: buyMarketplace, apply: flags.includes('--yes') || flags.includes('--apply'), }); break; }
case 'upload': if (!args[1]) { console.error('Usage: node keeps.mjs upload <piece>'); process.exit(1); } await uploadToIPFS(args[1]); break; case 'mint': case 'keep': { if (!args[1]) { console.error('Usage: node keeps.mjs keep <piece> [network] [--thumbnail] [--to=<address>] [--yes]'); process.exit(1); } const toFlag = flags.find(f => f.startsWith('--to=')); const recipientAddr = toFlag ? toFlag.split('=')[1] : null; await mintToken(args[1], { network: getNetwork(2), generateThumbnail: flags.includes('--thumbnail'), recipient: recipientAddr, skipConfirm: flags.includes('--yes') || flags.includes('-y') }); break; } case 'update': if (!args[1] || !args[2]) { console.error('Usage: node keeps.mjs update <token_id> <piece> [--thumbnail]'); process.exit(1); } await updateMetadata(parseInt(args[1]), args[2], { network: getNetwork(3), generateThumbnail: flags.includes('--thumbnail') }); break; case 'lock': if (!args[1]) { console.error('Usage: node keeps.mjs lock <token_id>'); process.exit(1); } await lockMetadata(parseInt(args[1]), { network: getNetwork(2) }); break; case 'burn': if (!args[1]) { console.error('Usage: node keeps.mjs burn <token_id>'); process.exit(1); } await burnToken(parseInt(args[1]), { network: getNetwork(2) }); break; case 'redact': { if (!args[1]) { console.error('Usage: node keeps.mjs redact <token_id> [--reason="..."]'); process.exit(1); } const reasonFlag = flags.find(f => f.startsWith('--reason=')); const reason = reasonFlag ? reasonFlag.split('=').slice(1).join('=') : 'Content has been redacted.'; await redactToken(parseInt(args[1]), { network: getNetwork(2), reason }); break; } case 'set-collection-media': { // Parse --name=<text>, --image=<uri>, --homepage=<url> and --description=<text> flags const nameFlag = flags.find(f => f.startsWith('--name=')); const imageFlag = flags.find(f => f.startsWith('--image=')); const homepageFlag = flags.find(f => f.startsWith('--homepage=')); const descFlag = flags.find(f => f.startsWith('--description=')); const name = nameFlag ? nameFlag.split('=').slice(1).join('=') : undefined; const imageUri = imageFlag ? imageFlag.split('=').slice(1).join('=') : undefined; const homepage = homepageFlag ? homepageFlag.split('=').slice(1).join('=') : undefined; const description = descFlag ? descFlag.split('=').slice(1).join('=') : undefined; if (!name && !imageUri && !homepage && !description) { console.error('Usage: node keeps.mjs set-collection-media [--name=<text>] [--image=<ipfs-uri>] [--homepage=<url>] [--description=<text>]'); console.error(''); console.error('Examples:'); console.error(' node keeps.mjs set-collection-media --name="KidLisp Keeps (Staging)"'); console.error(' node keeps.mjs set-collection-media --image=ipfs://Qm...'); console.error(' node keeps.mjs set-collection-media --image=https://oven.aesthetic.computer/keeps/latest'); console.error(' node keeps.mjs set-collection-media --homepage=https://keep.kidlisp.com'); console.error(' node keeps.mjs set-collection-media --description="KidLisp generative art collection"'); process.exit(1); } await setCollectionMedia({ network: getNetwork(1), name, imageUri, homepage, description }); break; } case 'lock-collection': await lockCollectionMetadata({ network: getNetwork(1) }); break;
case 'deprecate-staging': { const addressesFlag = flags.find(f => f.startsWith('--addresses=')); const addresses = addressesFlag ? addressesFlag.split('=').slice(1).join('=').split(',').map(v => v.trim()).filter(Boolean) : []; const replacementFlag = flags.find(f => f.startsWith('--replacement=')); const replacementContract = replacementFlag ? replacementFlag.split('=').slice(1).join('=').trim() : null; const burnLimitFlag = flags.find(f => f.startsWith('--burn-limit=')); const burnLimit = burnLimitFlag ? Number.parseInt(burnLimitFlag.split('=')[1], 10) : undefined; const apply = flags.includes('--yes') || flags.includes('--apply');
await deprecateStagingContracts({ network: getNetwork(1), addresses, replacementContract, burnLimit, apply, }); break; } case 'fee': // Show current keep fee const feeInfo = await getKeepFee(getNetwork(1)); console.log('\n╔══════════════════════════════════════════════════════════════╗'); console.log('║ 💰 Current Keep Fee ║'); console.log('╚══════════════════════════════════════════════════════════════╝\n'); console.log(` Contract: ${feeInfo.contractAddress}`); console.log(` Keep Fee: ${feeInfo.feeInTez} XTZ (${feeInfo.feeInMutez} mutez)\n`); break; case 'set-fee': { if (!args[1]) { console.error('Usage: node keeps.mjs set-fee <amount_in_tez>'); console.error(''); console.error('Examples:'); console.error(' node keeps.mjs set-fee 5 # Set fee to 5 XTZ'); console.error(' node keeps.mjs set-fee 0 # Free keeping'); console.error(' node keeps.mjs set-fee 0.5 # Set fee to 0.5 XTZ'); process.exit(1); } const feeAmount = parseFloat(args[1]); if (isNaN(feeAmount) || feeAmount < 0) { console.error('❌ Invalid fee amount. Must be a non-negative number.'); process.exit(1); } await setKeepFee(feeAmount, { network: getNetwork(2) }); break; } case 'withdraw': { const dest = args[1]; // Optional destination address await withdrawFees(dest, { network: getNetwork(dest ? 2 : 1) }); break; }
case 'cancel': case 'unlist': case 'delist': { if (!args[1]) { console.error('Usage: node keeps.mjs cancel <token_id|$piece> [network] [--yes]'); console.error('Retracts active Objkt listing. Works even if token has been transferred away.'); process.exit(1); } await cancelListing(args[1], { network: getNetwork(2), apply: flags.includes('--yes') || flags.includes('--apply'), }); break; }
case 'cancel:batch': { const positional = args.slice(1); let network = 'mainnet'; if (positional.length > 0 && ['mainnet', 'ghostnet'].includes(positional[positional.length - 1])) { network = positional.pop(); } if (positional.length === 0) { console.error('Usage: node keeps.mjs cancel:batch <token|$piece>... [network] [--yes]'); process.exit(1); } await cancelBatch(positional, { network, apply: flags.includes('--yes') || flags.includes('--apply'), }); break; }
case 'set-admin': { if (!args[1]) { console.error('Usage: node keeps.mjs set-admin <new_admin_address>'); console.error(''); console.error('This changes the contract administrator. Only the current admin can call this.'); console.error(''); console.error('Examples:'); console.error(' node keeps.mjs set-admin tz1abc... # Set new admin'); process.exit(1); } const newAdmin = args[1]; if (!newAdmin.startsWith('tz1') && !newAdmin.startsWith('tz2') && !newAdmin.startsWith('tz3')) { console.error('❌ Invalid Tezos address. Must start with tz1, tz2, or tz3.'); process.exit(1); } await setAdministrator(newAdmin, { network: getNetwork(2) }); break; }
// v4 NEW COMMANDS case 'royalty': case 'royalty:get': await getRoyalty(getNetwork(1)); break;
case 'royalty:set': { if (!args[1]) { console.error('Usage: node keeps.mjs royalty:set <percentage> [network]'); console.error(''); console.error('Examples:'); console.error(' node keeps.mjs royalty:set 10 # Set royalty to 10%'); console.error(' node keeps.mjs royalty:set 15 # Set royalty to 15%'); console.error(' node keeps.mjs royalty:set 0 # No royalties'); console.error(''); console.error('Maximum: 25%'); process.exit(1); } const percentage = parseFloat(args[1]); if (isNaN(percentage)) { console.error('❌ Invalid percentage. Must be a number.'); process.exit(1); } await setRoyalty(percentage, { network: getNetwork(2) }); break; }
case 'pause': await pauseContract({ network: getNetwork(1) }); break;
case 'unpause': await unpauseContract({ network: getNetwork(1) }); break;
case 'send': { if (!args[1] || !args[2]) { console.error('Usage: node keeps.mjs send <to_address> <amount_xtz> [network]'); console.error(''); console.error('Examples:'); console.error(' node keeps.mjs send aesthetic.tez 3 --wallet=staging --yes'); process.exit(1); } const sendTo = args[1]; const sendAmt = parseFloat(args[2]); if (isNaN(sendAmt) || sendAmt <= 0) { console.error('❌ Invalid amount.'); process.exit(1); } await sendTez(sendTo, sendAmt, getNetwork(3)); break; }
case 'transfer': { if (!args[1] || !args[2]) { console.error('Usage: node keeps.mjs transfer <token_id> <to_address> [network]'); console.error(''); console.error('Examples:'); console.error(' node keeps.mjs transfer 53 reas.tez --wallet=aesthetic --yes'); console.error(''); console.error('Automatically retracts active Objkt listing if one exists.'); process.exit(1); } const xferTokenId = parseInt(args[1]); const xferTo = args[2]; if (!xferTo.startsWith('tz') && !xferTo.endsWith('.tez')) { console.error('❌ Invalid Tezos address.'); process.exit(1); } await transferToken(xferTokenId, xferTo, getNetwork(3)); break; }
case 'transfer:admin': { console.error('⚠️ transfer:admin is deprecated (v10 contract has no admin_transfer).'); console.error(' Use: node keeps.mjs transfer <token_id> <to_address> --wallet=aesthetic'); process.exit(1); }
case 'help': default: console.log(`╔══════════════════════════════════════════════════════════════╗║ 🔮 Keeps - Tezos FA2 Contract Manager ║╚══════════════════════════════════════════════════════════════╝
Usage: node keeps.mjs <command> [options]
Commands: deploy [network] Deploy contract (default profile: v10) sync-secrets [network] Sync active contract/profile to Mongo secrets status [network] Show contract status balance [network] Check wallet balance wallets Show all wallet balances (XTZ, ETH, SOL, BTC, ADA) tokens [network] List tokens held by current wallet market [network] Show Objkt listings/offers/sales snapshot upload <piece> Upload bundle to IPFS mint <piece> [network] Mint a new keep sell <token> <price> [network] List token on Objkt (dry-run unless --yes) sell:batch <ref=price>... Batch-list multiple tokens on Objkt accept <offer_id> [network] Accept one active Objkt offer (dry-run unless --yes) accept:auto <ref=min>... Accept best offers above per-token thresholds buy <ask_id> [network] Buy a listed token (fulfill_ask, dry-run unless --yes) update <token_id> <piece> Update token metadata (re-upload bundle) lock <token_id> Permanently lock token metadata burn <token_id> Burn token (allows re-keeping piece) redact <token_id> Censor token (replace with redacted content) set-collection-media Set collection icon/description lock-collection Permanently lock collection metadata deprecate-staging [network] Pause + deprecate + burn staging contracts fee [network] Show current keep fee set-fee <tez> [network] Set keep fee (admin only) set-admin <address> Change contract administrator (admin only) withdraw [dest] [network] Withdraw accumulated fees to address cancel <token> [network] Retract active Objkt listing (works on dead listings) cancel:batch <token>... Cancel multiple listings in one pass
v4 Commands (Royalties, Pause, Admin Transfer): royalty [network] Show current default royalty percentage royalty:set <pct> [network] Set default royalty (0-25%, admin only) pause [network] Emergency pause (stops minting, admin only) unpause [network] Resume operations (admin only) transfer <id> <to> Transfer token (auto-unlists, supports .tez domains)
help Show this help
Networks: mainnet Tezos mainnet (default) ghostnet Tezos ghostnet (testnet)
Flags: --contract=<profile> Deploy profile: v10 | v9 | v8 | v7 | v6 | v5rc | v4 --thumbnail Generate animated WebP thumbnail via Oven and upload to IPFS (requires Oven service) --to=<address> Recipient wallet address (default: server wallet) --limit=<n> Max rows for tokens command --listings=<n> Max rows for market listings/offers --sales=<n> Max rows for market sales --marketplace=<KT1...> Override marketplace contract for sell/accept --replace Retract existing ask before listing new price --min=<xtz> Minimum bid filter for accept <offer_id> --referral-bps=<n> Referral bonus bps for marketplace ask (default: 500) --start=<iso8601> Scheduled listing start time --expiry=<iso8601> Listing expiry time --image=<uri> Collection image URI (IPFS or URL) --homepage=<url> Collection homepage URL --description=<text> Collection description --addresses=<KT1,KT1,...> Explicit contract list for deprecate-staging --replacement=<KT1...> Replacement contract for deprecation notice --burn-limit=<n> Limit number of burns per contract --yes / --apply Send live transactions for destructive commands
Examples: node keeps.mjs deploy mainnet --wallet=kidlisp --contract=v9 node keeps.mjs sync-secrets mainnet --contract=v9 node keeps.mjs deploy mainnet --wallet=kidlisp --contract=v8 node keeps.mjs deploy mainnet --wallet=kidlisp --contract=v7 node keeps.mjs deploy mainnet --wallet=kidlisp --contract=v6 node keeps.mjs deploy mainnet --wallet=staging --contract=v5rc node keeps.mjs deploy ghostnet --wallet=aesthetic --contract=v4 node keeps.mjs balance node keeps.mjs tokens --wallet=aesthetic node keeps.mjs market node keeps.mjs mint wand --thumbnail # With IPFS thumbnail node keeps.mjs mint wand --to=tz1abc... # Mint to specific wallet node keeps.mjs sell '$bip' 6 --wallet=aesthetic --yes node keeps.mjs sell:batch '$faim=5.5' '$tezz=5.8' '$bip=6' --wallet=aesthetic --yes node keeps.mjs accept 12179750 --wallet=aesthetic --min=13 --yes node keeps.mjs accept:auto '19=8.5' '20=9' '21=10' '22=10.5' '23=12' '24=13' --wallet=aesthetic --yes node keeps.mjs buy 12589569 --wallet=aesthetic --yes node keeps.mjs update 0 wand # Re-upload bundle & update metadata node keeps.mjs lock 0 # Permanently lock token 0 node keeps.mjs burn 0 # Burn token 0 (allows re-mint)
v4 Examples: node keeps.mjs royalty:set 10 # Set royalty to 10% node keeps.mjs royalty # View current royalty node keeps.mjs pause # Emergency pause node keeps.mjs unpause # Resume operations node keeps.mjs transfer:admin 5 tz1... tz1... # Emergency transfer # Fee management node keeps.mjs fee # Show current keep fee node keeps.mjs set-fee 5 # Set keep fee to 5 XTZ node keeps.mjs set-fee 0 # Make keeping free node keeps.mjs withdraw # Withdraw fees to admin wallet node keeps.mjs withdraw tz1abc... # Withdraw fees to specific address # Collection media (use live endpoint for dynamic thumbnail) node keeps.mjs set-collection-media --image=https://oven.aesthetic.computer/keeps/latest node keeps.mjs set-collection-media --homepage=https://keep.kidlisp.com node keeps.mjs set-collection-media --image=ipfs://QmXxx --description="KidLisp art" node keeps.mjs lock-collection # Lock collection metadata forever node keeps.mjs deprecate-staging --wallet=staging # Dry run node keeps.mjs deprecate-staging --wallet=staging --yes --replacement=KT1...
Environment: OVEN_URL Oven service URL (default: https://oven.aesthetic.computer)`); } } catch (error) { console.error(`\n❌ Error: ${error.message}\n`); const details = formatExtendedError(error); if (details) { console.error(`${details}\n`); } process.exit(1); }}
// Run CLI if executed directlyif (process.argv[1] === fileURLToPath(import.meta.url)) { main();}
// Export for use as moduleexport { createTezosClient, deployContract, syncCurrentContractToSecrets, getContractStatus, getBalance, getAllWalletBalances, listOwnedTokens, showMarketSnapshot, listTokenForSale, listBatchForSale, acceptOffer, acceptOffersAboveThreshold, buyToken, uploadToIPFS, mintToken, updateMetadata, lockMetadata, burnToken, redactToken, setCollectionMedia, lockCollectionMetadata, getKeepFee, setKeepFee, setAdministrator, withdrawFees, deprecateStagingContracts, detectContentType, loadCredentials, CONFIG};