正在打开 Ripples 上的发行
正在打开 Ripples 上的发行
所有密钥
权限范围白纸黑字
同一份列表还写明了两个目标金额、各项费用、创作者的收入、池的锁定方式,以及 Ripples 每把密钥能做什么、不能做什么。
从这里开始
Ripples 可以创建一个自带市场、从诞生那一刻起任何人都能交易的代币,一个可以直接购买的 NFT 合集,或者两者一起。整个产品就是下面四步,其余内容都是参考资料。
供应量、目标金额、费用、创作者的收入以及预留代币的释放安排,都在发行创建时写定。发行时锁定的内容列出了其中每一个数值、Ripples 部署的每一个地址,以及它持有的每一把密钥,并写明每把密钥能做什么、不能做什么。更详细的同一套说明见关于页面。
保护机制
发行或交易可能遇到的情况,以及每种情况下会发生什么。每个回答都链接到包含详细说明的章节。
每笔交易都带有你选择的滑点上限:0.5%、1% 或 2%。如果交易上链之前价格的变动超过这个幅度,交易会被拒绝,你投入的金额仍留在你的钱包里。
为 NFT 买家保留的代币会一直锁定,直到市场达到目标金额、流动性上线。之后,每位买家的份额在这次发行设定的锁定期内保持锁定,然后逐步解锁。每个发行在创建时设定两条规则之一,此后永远无法更改:要么领取权留在铸造该 NFT 的钱包,要么随 NFT 转移,由持有 NFT 的人领取尚未领取的代币。合集页面会注明适用哪条规则。创作者无需等市场达到目标金额,就能收到 NFT 的销售收入。市场募满时会发生什么
结算不需要 Ripples 的任何密钥。任何钱包都可以发起这次调用,Ripples 自己的机器人也会发起,但它的权限并不比其他钱包多。由发送者支付网络费。
Wake 的合约在支付那一刻读取谁持有这个 NFT,所以下一笔支付会给到新的持有者,包括出售之前累积的收入。卖家如果想要这部分收入,需要先收取。任何人都无法把 Wake 转到别处,任何人都可以触发支付,而支付只会给到持有者。如果发行所基于的智能体的 NFT 在另一条链上,创作者收入会留在发起发行的钱包,签名之前的核对页面会写明这一点。留下 Wake
智能体可以买入自己的代币、在你设定的上限内卖出、从自己的合集中铸造、向你指定的地址付款,以及写入它的运行日志。除此之外它什么都做不了。它的合约没有所有者、没有设置函数,也没有提取功能,所以这些限制在发行的整个生命周期内有效,任何人都无法提高,包括你自己。它铸造的作品会留在它手里,因为它无法转移 NFT。发行智能体
推文下方的回复会说明需要修改哪一部分,例如缺少图片,或者代币符号缺失、过长。每个账户每天可以发行 10 个,回复会说明下一个何时可以发行。通过推文发起的发行会先保留,直到创作者用一次签名认领它。从 X 推文发行
有些资产允许发行方暂停转账或封禁某个地址。限制生效期间,以该资产计价的市场上所有买入、卖出、铸造和转入池的操作都会停止,发行方解除限制后恢复。这项控制权只属于发行方。发行的费用
面向创作者
你需要一个浏览器钱包、一个名称、一个代币符号,以及足够支付发行费用的这条链的原生资产。任何创建代币的发行,还需要一张图片和一段简介。在 Robinhood 上,这些内容由代币本身保存:图片链接、简介和你的链接会在创建时写入代币,之后没有任何方式可以更改。Robinhood 上仅 NFT 的发行两者都不需要:它自带作品图。其他一切都在发行表单中决定;在请求钱包批准之前,表单会把这笔交易要花费的所有金额加总,并逐项列出。
可选的创作者费
创作者可以把未来创作者费收入的 0.01% 至 100% 分给代币持有者;如果发行带有合集,也可以分给合集作品的持有者。这个决定是永久的:手续费一旦转出,任何人都无法撤回,也无法改投别处,所以签名之前请把它视为最终决定。在 Robinhood 上,当前接收创作者费的钱包会在准备发行的分配合约时,设定分成比例和分给谁;其余手续费继续支付给创作者,或支付给创作者指定的收款地址。在那里开始的带合集发行,默认由作品持有者分享手续费:“确认”步骤会写明比例,这一行旁边的开关可以关闭它。在带 Foam 的发行中,符合条件的买入所铸造的作品与其他作品一样计算。仅准备分配合约不会改变任何事,真正转移手续费的是第二笔交易。
这与用股票代币为市场计价或记录 StockLink 是两回事。NFT 销售收入和转出之前已赚取的创作者余额也另行计算。奖励是实际赚取的资产:配对资产、发行的代币,或两者兼有。符合条件的余额按每个周期内的持有时长加权。没有固定收益率;没有可分配手续费的周期不支付任何奖励。
Ripples 读取公开的持仓历史,并为每个周期发布一份资金已到位的分配结果。Ripples 能用它做什么、不能做什么,写在条款中。
周期发布后,Ripples 会把每位持有者的份额发送到他们的钱包。网络费由 Ripples 支付。持有者无需做任何事。金额太小、不值得支付网络费的份额会留在“账户”中,等待领取。
“账户”会显示每个已发布的周期,以及两种资产各自按其单位计的金额;尚未到账的周期可以在那里领取。在请求钱包签名之前,Ripples 会对照链上数据核验这次领取,并拒绝已经支付过的领取。两种资产一起领取;如果所需的某笔转账受到发行方限制,领取会保持未执行,直到限制解除。你自己发起领取时,需要以这个网络的原生资产支付网络费。显示倍数只改变钱包中的显示,不改变转账金额。
公开的分配结果可以通过 GET /v1/holder-rewards 获取,参数为 rail、account 和可选的 cursor。完整的周期(epoch)证明使用 GET /v1/holder-rewards/epochs/{distributor}/{epoch}?rail=...。只有这条链自己的工厂合约或程序支持时,才提供奖励。本站的任何设置都无法开启它。
发行类型
代币发行会创建一种供应量固定的代币,以及一个任何人都可以买卖它的市场。NFT 合集发行会创建一次发售,买家直接从中购买。组合发行同时创建两者并把它们连接起来,Ripples 正是为这种发行类型而建的。
在组合发行中,每笔 NFT 付款都有一部分进入代币的市场,而不是归你;购买 NFT 的钱包会登记在册,获得一份代币供应量。这份代币不会立即发放:要等市场达到目标金额,然后逐步释放。每笔付款的其余部分归你,你可以随时从账户页面提取。
发行表单为此提供三种设置。它们的区别在于:每笔 NFT 付款有多少进入市场、为买家保留多少代币供应量,以及买家要等多久:
每个合集从创建那一刻起,就各自保存这四个数字,所以它们描述的是你今天创建的发行,而不是网络对所有人适用的规则。之后无法更改。
可以,在 Robinhood 上。任何钱包都能在任一条链上发行代币或 NFT 合集,在 Robinhood 上任何钱包还能创建组合发行:一个带有自己 NFT 合集的代币。
填写表单期间可以更改发行类型。发行一旦创建,就固定下来。
在 Robinhood 上用 ETH 支付时,钱包会依次请求:封装发行所需的金额、允许工厂合约使用它,然后创建发行;以其他资产计价的发行,还会为那种资产增加一次授权。在 Robinhood 上,组合发行是一笔不含开盘买入的交易,随后是一次上传作品图的签名,以及一笔把作品图关联到合集的交易。
Wake
在 Robinhood 上持有 NFT 的人,都可以用它发行一个市场,这个 NFT 会因此留下 Wake:这个市场的创作者收入,支付给 NFT 的当前持有者。NFT 售出后,Wake 也随之转移。卖方和买方都无法把它转到别处。
与此同时,这件作品会注册为一个 ERC-8004 智能体,以作品本身作为形象,并附上它在 8004scan 和 OpenSea 上的页面链接。持有者可以给它改名,也可以把它指向自己的端点。记录给这个智能体的反馈始终跟着智能体,因此也始终跟着这件作品。
持有智能体的钱包也可以用它开始发行,发行会沿用它的名称和形象。智能体的 NFT 在 Robinhood 上时,Wake 跟随这个 NFT;在其他链上时,创作者收入留给发起发行的钱包。
一个市场还可以把它的 Wake 分给起始合集中最多 256 件作品共享。在“确认”步骤的“高级”中打开“共享 Wake”,按代币 ID 列出这些作品,例如 1-20, 25;每件作品获得相等的一份。每件作品各自收取自己的那份,这份收入随作品一起易手,任何人都可以一次性向所有作品支付各自的份额。
Wake 由一个没有所有者、也没有任何设置项的合约支付,它在支付的那一刻读取 NFT 的持有者。任何人都可以触发支付,而支付只会发给持有者。每件留下 Wake 的作品都列在 Wake 页面上,并显示它的待支付金额和已支付金额。
Upwell
Upwell 是为一个币开设的 NFT 合集,这个币已经在 Robinhood 上的 Pons 交易。每次铸造都会在同一笔交易中为铸造者买入这个币,所以作品和币会一起到达铸造者的钱包。
每次铸造的价格中,80% 用于在这个币的 Pons 曲线上买入;币毕业后,则在它的 Uniswap v4 池中买入。合集创作者获得 15%,Ripples 获得 5%。Pons 的 1% 手续费以及币的创作者设定的任何税费都从这笔买入中扣除,所以铸造在签名之前就会显示能买到多少个币。铸造者签名时确认一个最低数量;如果价格变动超出这个界限,就不会铸造,也不会花费任何资金。
任何人都可以为一个币开设 Upwell,一个币可以有任意多个 Upwell。从 Upwell 页面开始:粘贴币的地址,添加作品图,然后设定至少 0.001 ETH 的价格和供应量。发行费用与任何合集相同,为 0.0005 ETH;创作者可以随时在合集页面提取自己的份额。同一页面上的 Upwell 排行榜按各个币的 Upwell 累计买入了多少来排名。
Upwell 买入的是由 Pons 发行、以 ETH 计价的币。Pons 仍在对一个新币的最初几笔买入征税时,铸造会等待;处于曲线和池之间的币,会在铸造时一并开启它的池。
工厂合约是 0x48Fb81eEaDC1A8Af9037abD7B23DCc2aF8554e33,买入币的合约是 0x61E8c408750FB6b608f58F6AF8Df30DE2F792b32,两者都已在 Sourcify 上完成源码验证。买入合约没有所有者,两次铸造之间不持有任何资产。
发行类型
智能体发行是智能体为自己开启的组合发行。系统会为它创建一个合约,并在同一笔交易中由这个合约开启发行,因此这个合约就是登记在案的创作者:每笔交易手续费的 70% 支付进这个合约,成为智能体的营运资金,而不是你的收入。
此后,智能体自己运营这个发行。它可以在自己的市场上买入自己的代币,在你设定的上限内卖回,从自己的合集中铸造,向你指定的地址付款,并写入它的公开运行日志。除此之外,它什么也做不了。合约没有所有者,没有设置函数,也没有提取功能,所以你设定的限额就是整个发行期间的限额;之后你和其他任何人一样,都无法提高它们。
你需要设定七项:操作智能体的钱包、发行时存入国库的运营资金、每日可支出的金额、单笔交易可支出的金额、每日可卖出多少自己的代币、每日可铸造多少件作品,以及它可以付款的地址。每日支出上限涵盖所有支出,所以买入、铸造和付款都从同一笔预算中扣除。卖出上限为零意味着智能体永远不能卖出自己的代币,表单默认就是这个设置。
和 Robinhood 上的其他发行一样,智能体发行需要钱包签名两次:一次是 ETH 的支出授权,一次是发行本身。合约、它的初始资金和发行都在同一笔交易中完成,所以不存在智能体持有资金却没有市场的状态。
由于一切都由合约持有,会带来两个结果。智能体从自己合集中铸造的作品会留在它那里,因为它可以向公开的地址付款,但无法转移 NFT。而作品图设置仍由你掌握:你通过智能体把合集指向它的图片并冻结它们,方式与其他任何发行相同。
从推文发行
在推文中标记 @ripplesdotrun,写上 launch,给出代币符号并附上一张图片。整张表单就是这些。系统会在 Robinhood 上创建一个代币和一个合集,图片成为作品图,你的推文下方会有一条回复附上链接。下面的其他内容都是可选的,可以按任意顺序、用你本来就会用的说法写出。只是谈论发行、没有写代币符号的推文,不会触发发行。
发行由 Ripples 创建,并为你保留,直到你认领。认领只需在推文串链接的认领页面上签名一次,之后创作者费和作品 #1 就归你。推文中写 for @someone,则改为替对方发行,由对方来认领。
$KOI。最多十个字母和数字,至少包含一个字母,因此代币符号可以以数字开头。纯数字会按数字读取,而不是代币符号,所以推文里写的价格仍是价格。推文中的第一个代币符号就是这次发行的代币符号。for @alice。回复会发给对方,由对方认领。33 pieces,也可以写 7 editions、12 nfts、5 items、9 pcs 或 x1000。从 1 起的任意数字都可以。不写则为 100 件。单独的数字会留在名称里,所以 50 Cent 是名称,而不是数量。paired with GLD,或 pair with、pair it with、pair it up with,加不加美元符号都可以。之后市场会以这只股票而不是 ETH 计价、交易和收取手续费。它必须是 Ripples 用来为发行计价的股票;超出这个范围的代币符号会收到说明原因的回复。如果这只股票当时没有市场,推文会收到回复,不会发行,等它开始交易后可以再发一次。silly mode 或 dark mode,写在推文任意位置。silly 模式会把每件作品生成得荒诞、出人意料、天马行空;dark 模式则生成诡异、不祥的作品,吓人但不血腥。不写则按常规方式生成。让它成为指令的是 mode 这个词,所以发行仍然可以叫 Dark Pepe。2% creator fee 或 creator fee of 2%。范围为 1% 至 10%,在所有人都要支付的 1% 之外,对每笔交易额外收取。不写则为 2%。half of that to holders,或 all、everything,或者写一个数字,例如 30% of that to holders。可以是你自己创作者费的 1% 至 100%,以发行的计价资产支付给持有者:也就是配对的资产,或 ETH。hey @ripplesdotrun launch $JPMEME JPEG Morgan for @alice, 33 pieces,
paired with GLD, 2% creator fee, with half of that going to holders这会创建一个名为 JPEG Morgan、共 33 件作品的合集,以黄金为交易对,每笔交易向创作者收取 2%,其中一半以黄金支付给持有者。由 Alice 认领。
承诺了分成的发行在创建时会获得两个专属合约。一个按推文指定的比例拆分每一笔创作者费,并把创作者的部分直接转出。另一个保管应付给持有者的款项,并支付出去。分成比例在发行创建时设定,之后没有任何函数可以更改。
每个钱包应得多少,按它持有的时长计算,而不是看某一时刻的持仓,所以在支付前刚买入几乎得不到什么。周期大约每天结束一次。应得的款项不会过期,可以在“账户”的“股票代币奖励”中领取,也可以由任何人代为推送给持有者。
无法发行的推文会收到回复,说明需要修改哪一部分:缺少图片、缺少代币符号或代币符号过长、这里不支持用来计价的股票、费用或分成超出范围、设置了分成却没有可用于支付的配对资产,或合集没有作品。每个账户每天最多发行十次,无论是为自己还是为别人;回复会说明下一次何时可以发行。
费用
发行费在链上设定,表单打开时读取,所以表单显示的就是实际会收取的费用,而不是写在这里的某个数字。下方的“批准时合计”会加总这笔交易要花费的其他所有金额,并逐项列出;如果钱包余额不够,表单不会提交。在 Robinhood 上,网络费不计入这个合计,而是另外以 ETH 支付,这一行会注明。
发行、铸造和交易都以 ETH 收费,每笔交易还要以 ETH 支付一小笔网络费。除了要花的部分,再多留一点 ETH。达到目标金额之前和之后,1% 的交易手续费都归 Ripples。这里的市场通过两种方式向你支付:一是你在发行时设定的创作者费,每笔交易都以 ETH 收取并全额支付给你;二是你的 NFT 销售收入。每个市场都沿用发行时的费率表,因此较早的市场可能有不同的费用、分配比例或目标金额。
市场也可以改用另一种经批准的资产计价,表单会列出可选的资产。目前 Robinhood 上的清单是 ETH、RIPPLES 和 PENGU,外加注册表批准的代币化股票。发行时锁定的内容列有当前的清单。这一选择在发行创建时就已固定。无论市场以什么资产计价,Robinhood 上的发行费始终以 ETH 收取,所以选择其他资产的创作者需要批准两项支出授权,而不是一项,每项都注明了用途。目标金额和开盘储备随后取自这种资产自己的那一行,并按它自己的小数位计算;上面的目标金额只适用于默认资产。如果某种资产的发行方可以暂停它或封禁地址,那么在限制生效期间,以它计价的市场上的买入、卖出、铸造以及转入池的操作都会停止。这两种控制都不属于 Ripples,也无法在这里解除。
作品图
每个 NFT 都指向一个小文本文件,文件里写着它的名称和图片链接。这些文件需要有人发布,在这里创建的发行由 Ripples 发布。
在“作品图”步骤中选择图片文件,Ripples 会发布它们,每个 NFT 对应一个编号文件,然后把合集指向这些文件。大型合集会分批上传;表单会显示进度,如果某一批上传失败,还会告诉你剩下哪些。
发行本身的图片取决于你发行的内容。单独发行的代币在“代币”步骤中设置图片;带合集的发行显示合集的第一件作品。只要发行会创建代币,Ripples 就会发布你填写的图片、简介和链接,并通过一个公开地址提供。
图片的方形版本要保持清晰:交易市场会把它用作合集头像,并裁掉非方形的部分。
之后你可以冻结作品图链接。冻结后,每个 NFT 读取详情的地址就此确定,你也放弃了自己更改这个地址的能力。托管在 Ripples 的作品,你将无法再替换或删除;违反 Ripples 条款的托管作品,Ripples 可以删除。
流动性
你的市场有一个目标金额。在达到目标之前,买家直接与市场本身交易,代币卖出越多,价格越高。市场持有的金额双向变动:买入会增加,卖出会减少,所以市场页面上的数字有升也有降。任何人都可以结算一个已达到目标的市场。Ripples 运行着一个自动结算的机器人,它的权限并不比其他任何钱包多。
结算的具体操作因链而异。在 Solana 上,今天创建的发行在 Meteora 的联合曲线上开盘;曲线中的资金达到 85 SOL 时,其储备和剩余供应量会按曲线达到的价格转入一个公开的 Meteora 池,流动性永久锁定。曲线募满后片刻之内,Ripples 就会完成转移,交易随即在池中继续。在 Ripples 程序上创建的发行则转入一个公开的 Raydium 池,锁定方式相同:它的目标金额包含创建这个池的成本,所以你看到的目标金额会略高于市场阈值;准备池的期间,交易会暂停片刻。
在 Robinhood 上,发行从第一个区块起就是一个公开的 Uniswap v4 池,因此无需转移。结算会在一笔交易中撤下募集期间为市场定价的头寸,并在同一个池中开启永久头寸,锁定方式按发行创建时声明的执行。触发结算的人支付网络费。
市场达到目标金额、流动性上线时,组合发行为 NFT 买家保留的代币也开始计时。在此之前不会释放任何代币。之后,每位买家的份额先在发行设定的锁定期内保持锁定,然后逐步解锁,直到全部可以领取。每个发行在创建时设定两条规则之一,此后永远无法更改:要么领取权留在铸造该 NFT 的钱包,要么随 NFT 转移,由持有 NFT 的人领取尚未领取的代币。合集页面会注明适用哪条规则。
流动性上线不会改变市场支付给你的收入。在 Solana 上,你的交易手续费分成从第一笔交易起就归你;达到目标后,锁定的池收取的手续费中,同样比例归你。你的账户页面会显示待收取的金额,并可在那里收取。在 Robinhood 上,达到目标金额时什么都不会改变:1% 的交易手续费仍归 Ripples,你的创作者费也照常收取。那里的池本身不收取手续费,因此没有池手续费可分。每个市场都沿用发行时的费率表,因此较早的市场可能有不同的费用、分配比例或目标金额。
你的 NFT 销售收入与以上这些都无关。它不需要等到达到目标,也不会在达到目标时停止。只要有人买入,这笔钱就归你,随时可以在账户页面收取。
收益
发行为你带来的所有收益都在同一个地方收取:你的账户页面。连接创建发行的钱包后,第一张卡片会显示那条链上可以收取的金额,用一个按钮就能全部收取。
创作者费随每笔交易累积,NFT 售出后销售收入即归你。两者在卡片上各占一行,以赚取时的资产显示。这些收益都不会过期,你可以在方便的时候收取。
在 Robinhood 上,智能体可以用 SDK 中的 collect,或通过 MCP 服务器调用 ripples_collect,收取同样这些收益。
发行之后
有两个链接最重要。合集页面是购买 NFT 的地方,市场页面是交易代币的地方。两个页面都无需连接钱包就能查看,所以任何人在做决定之前,都能先看到价格、供应量,以及市场已达到目标金额的多少。
把下面四点讲清楚,因为买家无法从一张图片上看出来:
不要承诺价格、回报,也不要承诺任何人一定能卖出。这些都不由你控制。你能承诺的是合约会做什么;两个页面上的每个数字都从链上读取,任何人都可以核实你说的话。
市场达到目标金额后,页面会链接到池。这时很适合再发一次帖:届时流动性已经锁定,而这正是谨慎的买家首先会查看的。
Robinhood 上的合集使用标准的 ERC-721 和 ERC-7572 接口,因此索引这条链的交易市场可以上架它们。这项支持的最新情况,请在 OpenSea 上查看。
Robinhood
下面的每个数值都在发行创建时设定,并由 Robinhood 上的合约强制执行,没有一项是靠政策维持的。凡是 Ripples 持有密钥的地方,对应条目都写明了这把密钥能做什么、不能做什么。
地址
Ripples 部署或持有的每一个地址,都完整列在这里。Ripples 只在这里公布自己的地址,不在其他任何地方公布,所以如果有人以 Ripples 的名义发给你一个不在这份列表上的地址,那它并非来自 Ripples。在 Ripples 上创建的发行各有自己的地址,显示在各自的页面上。Robinhood 上的交易还会经过 Uniswap 的池管理合约(PoolManager)和通用路由合约(Universal Router)结算,这两个合约不是 Ripples 部署的。下方的 Robinhood 参考列出了这个路由合约的地址,上面的毕业 Hook 合约会返回池管理合约的地址。结算一个已达到目标金额的市场不需要 Ripples 的任何密钥,因此这里没有为它列出钱包:任何人都可以发起这次调用。
集成页面以机器可读的形式提供同样的地址,以及事件主题和读取市场的调用。
已批准资产
每个发行以一种资产结算。这种资产在创建时选定,并与本节的其他内容一起锁定。所有者维护着一份新发行可以选用的资产列表。这份列表只是一份资产列表:它不规定谁可以交易、持有或查看一个市场,Ripples 也没有在任何地方设置资格或司法辖区限制。下面的条目抄录自部署记录,而不是从链上读取,因此以最先列出的注册表为准,每一行都注明了资产获批时所在的区块。
没有指定资产的发行会使用链自身的资产以及表单自动填入的数值,其中包括下方的目标金额。指定其他资产的发行,必须按注册表中的记录申报这种资产的条目,否则工厂合约会拒绝创建。这里不重复列出这些数值:你选择资产时,表单会读取对应条目,并且不允许发行申报一个它未能读取的条目。两个条目也不能直接按数字比较,因为每个条目都以各自资产的小数位数计。
旁边的股票关联注册表只记录一种关联,仅此而已。它存储一个地址,不验证发行方或相关金融工具的任何信息,不转移任何资金,写入后也无法更改或删除。关联和计价资产是两个独立的选择:一个发行可以只有其中之一,也可以两者都有或都没有。
组合发行
组合发行是相互依存的一个代币和一个 NFT 合集。市场募集期间每次铸造用于买入代币市场的比例、这些铸造者可获释放的供应量份额,以及释放计划,都写入发行本身,创建后创作者和 Ripples 都无法更改。
市场
达到目标金额之前,买家直接与市场本身交易。达到目标金额时,头寸按市场最后一次成交的价格,进入发行创建时声明的锁定。在 Robinhood 上,发行本身就已经是一个公开的 Uniswap v4 池,因此结算会退出募集期间为市场定价的头寸,并在同一个池中开启永久头寸。下面的数值是发行表单目前设定的数值。更早创建的市场保留它创建时的目标金额、手续费和分成,并在自己的页面上写明。任何人都可以结算一个已完成的市场。Ripples 运行着一个自动执行结算的机器人,它的权限并不比其他任何钱包多。
totalBurned() 只统计经由销毁合约销毁的代币,包括它执行过的每一次回购。两者是不同的总数。转入销毁地址并不会减少代币的 totalSupply()创作者收入
创作者的收入来自铸造、交易,或两者兼有。交易收入从发行的第一笔交易开始,达到目标金额后也不会停止。下面的条目就是完整的答案。每个发行在创建时固定自己的数值,所以较早的发行保留它创建时的数值,并在自己的页面上显示。
密钥
Ripples 从不接收任何钱包密钥,也从不签署任何发行、铸造或交易。它实际持有的每一把密钥都列在这里,并写明这把密钥能做什么、不能做什么。
正在从 Robinhood 读取所有者。
目前的限制
以下是目前已部署内容的限制。写在这里,是为了不让任何人在阅读锁仓合约时,读出一个本节并未做出的承诺。
这一切背后的合约名称,见下方的 Robinhood 参考。Genesis 发行见 Genesis 页面。
第一条链 · Solana Mainnet
浏览器依据随代码提交的 Ripples 程序描述(即它的 Anchor 接口定义,IDL),用 Solana 的 @solana/kit 库构建交易。兼容的钱包通过 Solana Wallet Standard 审阅并签名这些交易。公开 API 为程序账户建立索引,为发行表单发布发行图片和详情,并保存已签名的 NFT 揭晓设置。另有一项独立的 Virtuals 智能体集成,支持一个专用的智能体钱包。它只能读取,不能做别的。智能体交易、付费服务和主网访问均未开启。
部署
engine 为 dbc;Ripples 程序上的发行,engine 为 ripples 或不存在。请以记录为准,不要按日期判断:两类发行各有自己的读取、交易和领取方式QuoteAsset 账户,由配置权限方写入。发行在创建时会记录 mint、它的代币程序和小数位,因此请读取发行本身,而不是某个列表NEXT_PUBLIC_LAUNCHPAD_CHAIN=solana
NEXT_PUBLIC_LAUNCHPAD_CLUSTER=mainnet-beta
NEXT_PUBLIC_LAUNCHPAD_PROGRAM_ID=RippcKjgg9pkjHY7RKp6R1K8jPGwq2BRSJcgbErufjF
NEXT_PUBLIC_LAUNCHPAD_API_BASE=https://api.ripples.run
NEXT_PUBLIC_ALLOW_MAINNET=1浏览器通过位于 https://api.ripples.run/v1/rpc 的注册表中继读取 Solana Mainnet,服务商密钥保存在中继上;每笔交易也通过轮询同一个中继来确认。页面不会与任何 Solana 节点建立 WebSocket 连接。没有 API base 时,本地构建读取该集群的公共端点 api.mainnet-beta.solana.com。
NEXT_PUBLIC_LAUNCHPAD_RPC_URL 指定页面优先读取的端点,中继在其后作为后备。所有 NEXT_PUBLIC_ 变量都能在页面中读到,因此如果端点的查询字符串、路径或用户名和密码中带有密钥,构建会拒绝它。中继每个请求只接受一次 JSON-RPC 调用,允许页面所需的读取方法以及 sendTransaction,并且只对 Ripples 程序响应 getProgramAccounts。NEXT_PUBLIC_LAUNCHPAD_RPC_FALLBACK_URL 指定另一个不同的第二端点;设为 off 则只从第一个端点读取。
交易
SOL 费用由已连接的钱包支付。请求签名之前,应用会检查转出的金额、让新账户保持有效所需的最低 SOL(租金豁免),以及一笔网络费缓冲。@solana/kit 的规划器会获取最近的区块哈希、模拟交易,并留出计算单元的安全余量。签名之后,应用会显示交易签名,并持续检查其状态,直到交易确认或 lastValidBlockHeight 过期。
const message = await client.planTransaction(instructions);
const transaction = await signTransactionMessageWithSigners(message);
const signature = getSignatureFromTransaction(transaction);
await sendWithoutConfirming(transaction, {
commitment: "confirmed",
preflightCommitment: "confirmed",
skipPreflight: false,
});
// Poll signature status and block height using
// message.lifetimeConstraint.lastValidBlockHeight.池创建之前,代币价格以 SOL 报价。只有当买家还没有标准代币持有账户(关联代币账户)时,买入才会为其创建一个。Ripples 程序上发行的代币使用 9 位小数,Meteora 曲线上的使用 6 位,钱包对完整的交易签名。
智能体集成
Virtuals 的 Agent Commerce Protocol(ACP)让智能体买卖服务。Ripples 的私有服务把一个已注册的 Virtuals 智能体和一个专用 Solana 钱包,连接到所选网络、已配置的 Ripples 程序和已提交的 Anchor IDL。它只接受一份限定的 Ripples 操作列表,拒绝任意转账、针对其他程序的指令、Virtuals 的代币发行工具、通用交易,以及程序权限或维护类操作。
发送交易之前,服务会确认两个 Solana 端点都在预期的网络上。它依据 Anchor IDL 解码每条指令,并检查程序地址、账户顺序、读写权限、必需的签名者、Ripples 账户的当前状态,以及单次操作和单日的支出上限。每个请求都会得到一个稳定的请求指纹(用于识别同一请求的摘要)和一条 Solana memo。结果不明确时,服务会暂停并检查已有的交易,而不是重新发送。
可选的卖方服务(ACP offering)是一项固定价格、以 USDC 支付的服务,名为 ripples-action-v1。买方为任务付款后,服务会在执行前一刻再次读取并检查最新的请求。转入 Ripples 的 SOL、Solana 网络费和新账户所需资金,都由智能体钱包支付。Virtuals 的赞助不覆盖 Ripples 程序交易。
这项集成是只读的。智能体交易、付费卖方服务和主网访问均未开启。情况有变时,本节会写明。
你自己的智能体
已经持有钱包的智能体不经过上面的 Virtuals 服务。它从公开 API 读取发行,通过注册表中继或自己的节点读取链上状态,构建与浏览器相同的指令,并用自己的密钥签名。所有网络费和每一笔交易都由这个钱包支付。本页没有任何部分会代它签名、付款或批准。
GET https://api.ripples.run/v1/launches 按从新到旧列出 Solana Mainnet 上的代币发行,返回 count、nextOffset 和 launches 数组。每一行包含 launch(市场地址)、mint、creator、name、symbol,组合发行还带有 collection。/v1/collections 对 NFT 合集做同样的事,/v1/launches/<address> 返回单个发行及其当前市场状态。列表默认返回 Solana Mainnet,传入 ?chain=robinhood 时返回 Robinhood;单个发行的路由会按传入的地址格式读取,因此两条链都能查询。/v1/collections 只索引 Solana Mainnet。Robinhood 上的发行也可以直接从工厂合约读取,发行列表就是这样做的。
const base = process.env.RIPPLES_API ?? "https://api.ripples.run";
const page = await fetch(`${base}/v1/launches?offset=0`).then((r) => r.json());
for (const row of page.launches) {
// row.launch, row.mint, row.creator, row.name, row.symbol, row.collection
}
// An index with nothing in it is a valid answer, so read the first row only if there is one.
const [first] = page.launches;
const one = first && (await fetch(`${base}/v1/launches/${first.launch}`).then((r) => r.json()));每条链单独计数,在链上出现第一个发行之前都返回 count: 0,所以循环要能处理空页。/v1/launches 的 limit 默认为 50,最大 200;/v1/collections 的 limit 固定为 25;offset 从最新一行开始计数。每个客户端地址每分钟最多读取 120 次,每个响应都会在 x-ratelimit-remaining 中给出剩余次数。
想在测试网络上运行同样的调用时,把 RIPPLES_API 指向你自己构建的 API。
读取链上状态时,POST https://api.ripples.run/v1/rpc 每个请求转发一次 JSON-RPC 调用,发往本站读取所用的 Solana Mainnet 端点。它接受客户端所需的读取方法(getAccountInfo、getMultipleAccounts、getLatestBlockhash、simulateTransaction、getSignatureStatuses 等)以及 sendTransaction。getProgramAccounts 只对 Ripples 程序响应,批量请求一律拒绝。中继对每个客户端地址的限制是每分钟 600 次调用,轮询比这更频繁的智能体应自备端点。
在 Solana Mainnet 上,用已发布的 IDL solana/idl/ripples.json 和上表中的程序地址构建买入指令。用 Codama 从它生成客户端,就能得到浏览器所用的同一个 getBuyInstructionAsync。发行账户写明了 mint、金库,以及发行向创作者付款的地址;配置账户写明了国库;价格是基于发行虚拟储备的恒定乘积曲线,因此在签名之前就能算好成交下限。回购金库是客户端自行派生的程序地址;其余每个账户都能在这两份记录中找到。
如果发行的持有者分得一部分创作者收入,发行会把这一比例记为 holderShareBps。当它大于零,且发行向创作者付款的地址不是它自己的分配账户时,买入或卖出指令还要在指令自身的账户之后附上可写的分配账户;如果发行以代币计价,还要在其后附上分配账户的托管账户。两者都是由发行派生的程序地址:["holder_rewards", launch] 和 ["escrow", quote_mint, distributor]。在这类发行上,程序会拒绝缺少它们的交易。
import {
appendTransactionMessageInstructions, createKeyPairSignerFromBytes, createSolanaRpc,
createTransactionMessage, getBase64EncodedWireTransaction, pipe,
setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash,
signTransactionMessageWithSigners,
} from "@solana/kit";
import { findAssociatedTokenPda, TOKEN_PROGRAM_ADDRESS } from "@solana-program/token";
import { fetchConfig, fetchLaunch, findConfigPda, getBuyInstructionAsync } from "./generated/ripples";
const rpc = createSolanaRpc("https://api.ripples.run/v1/rpc");
const agent = await createKeyPairSignerFromBytes(agentSecretKey);
const launch = await fetchLaunch(rpc, launchAddress);
const [configAddress] = await findConfigPda();
const settings = await fetchConfig(rpc, configAddress);
const [buyerToken] = await findAssociatedTokenPda({
owner: agent.address,
mint: launch.data.mint,
tokenProgram: TOKEN_PROGRAM_ADDRESS,
});
// The platform's fee and the creator's own charge come off the same base, then the
// constant-product output. In the first 3 seconds the opening tax comes off
// before both, so a floor sized here fails until that window closes.
const quoteIn = 100_000_000n;
const fee = (quoteIn * BigInt(launch.data.tradeFeeBps)) / 10_000n;
const creatorTax = (quoteIn * BigInt(launch.data.creatorTaxBps)) / 10_000n;
const net = quoteIn - fee - creatorTax;
const expected = (launch.data.virtualToken * net) / (launch.data.virtualQuote + net);
const minTokensOut = (expected * 9_900n) / 10_000n;
// Where this launch pays its creator. The account is pinned to that address, so it
// cannot be derived, and a launch created before the field existed carries the default
// address there and is paid at launch.creator instead.
const creatorPayout =
launch.data.creatorPayout === "11111111111111111111111111111111"
? launch.data.creator
: launch.data.creatorPayout;
const buy = await getBuyInstructionAsync({
launch: launchAddress,
mint: launch.data.mint,
tokenVault: launch.data.tokenVault,
buyerToken,
buyer: agent,
treasury: settings.data.treasury,
creatorPayout,
quoteIn,
minTokensOut,
});
const { value: latest } = await rpc.getLatestBlockhash().send();
const message = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayerSigner(agent, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(latest, m),
(m) => appendTransactionMessageInstructions([buy], m),
);
const signed = await signTransactionMessageWithSigners(message);
const signature = await rpc
.sendTransaction(getBase64EncodedWireTransaction(signed), { encoding: "base64" })
.send();在 Robinhood 上,市场是一个 Uniswap v4 池,所以智能体用 viem 从 Hook 合约读取价格,并通过 Universal Router 兑换,所用地址来自 Robinhood 部署表。路由合约经由 Permit2 支出,所以要先把计价代币批准给 Permit2,再由 Permit2 批准给路由合约。网络费以 ETH 从同一个钱包支付。
import { createPublicClient, createWalletClient, http, parseAbi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const chain = {
id: 4663,
name: "Robinhood",
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },
};
const account = privateKeyToAccount(agentPrivateKey);
const client = createPublicClient({ chain, transport: http() });
const wallet = createWalletClient({ account, chain, transport: http() });
const QUOTE = parseAbi(["function approve(address spender,uint256 value) returns (bool)"]);
const PERMIT2_ABI = parseAbi([
"function approve(address token,address spender,uint160 amount,uint48 expiration)",
]);
const ROUTER = parseAbi([
"function execute(bytes commands,bytes[] inputs,uint256 deadline) payable",
]);
// Price the market off LaunchLens.launchesOf and take the hook's cut off the output
// before signing a floor. The Robinhood "Token trading" section shows that
// arithmetic in full, and where the lens is deployed.
const quoteIn = 10n ** 16n;
const amountOutMinimum = floorFor(quoteIn);
const expiry = Math.floor(Date.now() / 1000) + 3_600;
await wallet.writeContract({ address: quoteToken, abi: QUOTE, functionName: "approve", args: [permit2, quoteIn] });
await wallet.writeContract({ address: permit2, abi: PERMIT2_ABI, functionName: "approve", args: [quoteToken, universalRouter, quoteIn, expiry] });
const hash = await wallet.writeContract({
address: universalRouter,
abi: ROUTER,
functionName: "execute",
args: [
"0x10",
[encodeV4Swap({ poolKey, zeroForOne: quoteIsCurrency0, amountIn: quoteIn, amountOutMinimum })],
BigInt(expiry),
],
});
await client.waitForTransactionReceipt({ hash });代币发行
在标准网页流程中,创作者填写名称、代币符号、简介、图片,以及可选的网站、X、Telegram 和自定义链接。Ripples 把它们一并发布,再自动填入指令的 uri 字段。自建界面的开发者可以直接提供兼容的公开元数据 URI。
createLaunch 指令创建三条核心记录:市场状态(发行的程序派生地址,即 PDA)、标准 Solana 代币(SPL Token mint),以及由 Ripples 控制的交易库存(程序金库)。全部代币供应量一次性发行,随后移除增发和冻结代币的权限。Metaplex Token Metadata 保存代币的名称、代币符号和 URI。
import { getCreateLaunchInstructionAsync } from "./generated/ripples";
const instruction = await getCreateLaunchInstructionAsync({
launch,
tokenVault,
creatorToken,
creator: walletSigner,
treasury,
args: {
nonce,
name: "Ripple",
symbol: "RPL",
uri: "ipfs://…",
curveSupply: 800_000_000n * 1_000_000_000n,
lpSupply: 321_052_631n * 1_000_000_000n,
virtualQuote: 36_250_000_000n,
virtualToken: 1_073_000_000n * 1_000_000_000n,
graduationQuote: 29_000_000_000n,
lpUnlockAt: 9_223_372_036_854_775_807n,
linked: false,
nftAllocationBps: 0,
mintToCurveBps: 0,
vestingCliff: 0n,
vestingDuration: 0n,
initialBuy: 0n,
minTokensOut: 0n,
snipeExemptions: [],
creatorTaxBps: 0, // the creator's own charge, 0 to 1000 bps
},
});纯代币发行开盘时的虚拟计价储备是目标金额的 0.4 倍,不能接收来自 NFT 的注资。联动发行使用同一目标金额的 1.25 倍,并保留由 NFT 资金推动毕业所需的更大流动性份额。对于白名单中的代币计价资产,请读取它当前的 QuoteAsset 记录:其中保存的储备就是联动发行的储备。独立发行的储备是目标金额的五分之二,只有当两个比例都能用原始计价单位精确表示时才可用。程序在创建时检查这些条款。之后对已批准资产的修改只适用于以后的发行,不会改变已有市场的储备或目标金额。
市场的手续费不是参数。程序会把 1% 写入它创建的每一个发行,调用方无法以其他条款开设市场。creatorTaxBps 是在此之上的创作者费,全额付给创作者,上限由程序限定。它排在参数的最后,而不是手续费腾出的位置,这样按旧参数列表构建的客户端会解码失败,而不会发出一笔没人设置的费用。
当实际 SOL 储备足以覆盖目标金额(graduationQuote)加上预留的建池成本(graduationCostLamports)时,就可以转入 Raydium 流动性池。可选的首次买入(initialBuy)与创建在同一笔交易中完成。
与 NFT 合集联动的代币从等待状态(LinkPending)开始。在 createLinkedCollection 成功之前,交易保持关闭。如果这第二笔交易始终没有完成,创作者可以调用 cancelPendingLaunch。
NFT 合集
每个 NFT 都是一个 Metaplex Core 资产,位于它所属合集自己的 Core 合集账户中,因此任何读取 Metaplex Core 的钱包或 NFT 市场都能显示它。Solana 上的 NFT 市场逐个收录合集,而本站的合集页面从 NFT 铸造的那一刻起就会显示它们。
createCollection 创建 Ripples 合集状态及其 Metaplex Core 合集账户。mintAsset 每笔交易创建一个 NFT,记录 SOL 付款,更新买家的铸造额度;如果是组合发行,还会为原始铸造者记录其对代币市场的注资。
| 指令 | 权限方 | 用途 |
|---|---|---|
mintAsset | 铸造者 | 支付 SOL,创建一个 NFT。 |
revealAsset | 揭晓权限方 | 替换当前的占位内容,只能执行一次。 |
setBaseUri | 创作者 | 更改预先准备的合集读取作品图所用的链接,锁定之前可用。 |
setPlaceholderUri | 创作者 | 在冻结之前更改当前的占位内容。 |
freezeMetadata | 创作者 | 永久关闭对 URI 的修改。 |
creatorWithdraw | 创作者 | 只提取归属于创作者的销售收入。 |
直接转入合集程序派生地址(PDA)的 SOL 不计为 NFT 付款。另有一条任何人都能调用的找回指令,只能把这部分多出的 SOL 转给记录在案的创作者。
股票关联
代币发行、NFT 合集或组合发行可以选择创建一个不可更改的 StockLink 附属账户。它记录关联主体、确切的 mint 地址,以及 SPL Token 或 Token-2022 程序。创建时会验证 mint 账户存在、已初始化,并且归所记录的代币程序所有。附属账户不改变其主体账户,也不改变发行的经济条款。
| 发行类型 | StockLink 主体 |
|---|---|
| 代币发行 | Launch PDA |
| NFT 合集 | Collection PDA |
| 组合发行 | Launch PDA |
程序验证的是 mint 账户的归属关系,不验证发行方,也不验证这个 mint 追踪的是什么。Ripples 自带的主网选择器从发行方目录中解析代币化追踪资产的 mint。目录不可用时,无法创建链接,内置快照也始终明确标为未验证。公开页面在把某个链接称为与发行方目录匹配之前,会重新精确查找这个 mint,并检查所要求的 Token-2022 程序。直接调用程序的开发者可以关联其他有效的 SPL Token 或 Token-2022 mint,但 Ripples 自带的界面会把它标为未验证。
股票链接只记录一个引用。它不转移任何资产,也不产生任何权利。链接不会买入或兑换追踪代币,不会把它与发行配对,不托管资产,不奖励持有者,不赋予分红权利,也不分配任何资产。它不改变任何经济条款:记录了链接的市场,仍保留创建时的计价资产、目标金额、手续费和释放条款,读取附属账户也不会触及其中任何一项。Ripples 自带的选择器在测试网络上不可用。
发行以创建时指定的资产结算:除非创作者在创建时指定了白名单中的 mint,否则就是 SOL。这个选择在同一份表单中完成,除此之外与 StockLink 无关:一个发行可以关联追踪资产并以 SOL 结算,可以以白名单中的 mint 结算而不关联任何东西,可以两者都选,也可以都不选。记录关联绝不会改变交易结算所用的资产,用某个 mint 为市场计价也不会记录与其发行方的任何关联。市场页面会写明一个发行带有两者中的哪一个,或者都没有。
能否获得所关联的代币化追踪资产,或发行结算所用的资产,取决于发行方的条款和用户所在的司法辖区。这是关于资产及其发行方的披露。Ripples 不设任何资格或司法辖区限制:程序、本站和本参考文档都不会检查读者身在何处或是谁,白名单只是一份资产列表。唯一的限制是发行方自己的暂停和账户冻结,这些限制是读取到的,而不是 Ripples 施加的;以已暂停 mint 计价的市场会一直停止,直到发行方解除暂停。
代币买卖
Meteora 曲线上的发行(engine: "dbc")通过 Meteora 自己的程序和 SDK @meteora-ag/dynamic-bonding-curve-sdk 交易。池地址就是市场:针对池及其配置获取报价,再带上报价给出的下限发送兑换。每笔交易都以 SOL 支付手续费,而且手续费在市场开盘后的最初一段时间里逐步降低,所以每笔交易都要单独报价,不要假定费率。曲线持有 85 SOL 后,程序会拒绝后续兑换,直到池开放;此后由 Jupiter 为这个代币提供路由。
const client = new DynamicBondingCurveClient(connection, "confirmed");
const virtualPool = await client.state.getPool(pool);
const config = await client.state.getPoolConfig(virtualPool.poolState.config);
const quote = client.pool.swapQuote2({
virtualPool, config,
swapBaseForQuote: false, // true to sell
hasReferral: false, eligibleForFirstSwapWithMinFee: false,
currentPoint: await getCurrentPoint(connection, config.activationType),
slippageBps: 100, swapMode: SwapMode.ExactIn, amountIn: lamports,
});
const transaction = await client.pool.swap2({
owner, pool, swapBaseForQuote: false, referralTokenAccount: null,
swapMode: SwapMode.ExactIn, amountIn: lamports, minimumAmountOut: quote.minimumAmountOut,
});Ripples 程序上的发行,用下面的指令与其发行账户交易。
// Amounts are base units: the quote's own smallest unit, and 9-decimal SPL units.
// A launch quoted in SOL takes lamports here; one quoted in a mint takes that mint's.
// creatorPayout is where this launch pays its creator, pinned by the launch record:
// launch.creatorPayout, or launch.creator where that field is the default address.
// A launch with launch.holderShareBps above zero that pays its creator anywhere other than
// its distributor also takes the distributor, writable, after the instruction's own
// accounts, and on a token-priced launch the distributor's escrow after that.
await getBuyInstructionAsync({
launch,
mint,
tokenVault,
buyerToken,
buyer: walletSigner,
treasury,
creatorPayout,
quoteIn: 100_000_000n,
minTokensOut,
});
await getSellInstructionAsync({
launch,
launchAuthority,
mint,
tokenVault,
sellerToken,
seller: walletSigner,
treasury,
creatorPayout,
tokensIn,
minQuoteOut,
});发行在创建时固定 tradeFeeBps,并在市场存续期间一直保持,所以请从发行账户读取,不要假定费率。quoteMint、quoteTokenProgram 和 quoteDecimals 也以同样的方式固定,所以计价资产同样要从发行上读取。在支持按发行指定资产之前创建的发行,其计价资产显示为空,表示 SOL。网页表单创建的 Solana 市场手续费为 1%,按手续费本身的固定比例分配:交易量的 0.7% 归创作者,0.24% 留作 $RIPPLES 回购,其余归协议。创作者的分成从发行的第一笔交易起支付;毕业后,collectLockedFees 按同一记录比例分配锁定池头寸收取的费用。请从发行账户的 creatorFeeShareBps 读取这一比例:按更早费率表创建的市场会返回它自己的比例。发行还可以固定一项创作者费 creatorTaxBps,买入和卖出时都从与手续费相同的金额中扣除,全额归创作者,协议不分成。请从发行上读取它,并在扣除它之后计算成交下限,就像上面的智能体买入示例那样。在最初的 3 秒内,非豁免买家还要支付开盘税,税率从 99% 逐步降到零。这笔税不买入任何代币。它由发行以 snipeTaxCollected 保存,在毕业时注入池中,并且有意不计入 realQuote(),因此也不计入目标金额。签名者始终要提供价格限制:买入时为 minTokensOut,卖出时为 minQuoteOut。
市场把创作者收入付到发行账户上的 creatorPayout。它最初就是创作者本人,只有其中记录的地址才能用 setCreatorPayout 更改它,更改从下一笔交易起生效。这个地址无法直接接收的收入会留在托管账户中,而不会变成协议收入:claimEscrow 支付某一种资产的一笔余额,claimNativeEscrow 支付 SOL。两者都无需许可,也都不能改变付款去向。如果一笔 SOL 领取金额太小,不足以让收款方超过自身的租金下限,就会被拒绝,因为运行时会回收它所落入的余额。
组合发行
Raydium 池创建之前,每笔 NFT 付款中按配置比例的部分会转入代币市场的储备,但不改变它用于定价的虚拟储备。铸造者的份额在发行毕业时确定:按其铸造转入市场的金额按比例计算。池建成后,claimVested 按配置的锁定期和释放计划释放代币。铸造者全部领取之后,finalizeBeneficiary 会为其结清,最后的舍入零头也因此得以入账。出售或转让 NFT 不会转移这份代币领取权。
Meteora Dynamic Bonding Curve(DBC)上的组合发行运作方式不同,相关指令和接口的名称中都带有 dbc。池由三份 Ripples 配置之一创建,每份对应下面列出的一种设置。每种设置固定了为合集预留多少代币供应量,以及这部分如何解锁。随后用 create_dbc_linked_collection 把合集与池关联,合集在池创建 31 秒后开售,此时曲线开盘最初几秒的较高手续费已经结束。在此之前发出的铸造会被拒绝。曲线完成之前,每次铸造(mint_asset_dbc)会把价格中按配置的部分,通过 Ripples 程序在曲线上买入代币。曲线完成之后,全部铸造价格都是合集的收入。曲线迁移到 Meteora 池后,任何人都可以调用 settle_dbc_link,把这些铸造买入的代币与预留的供应量合并。Ripples 运行着一个调用它的服务,所以集成方通常不需要自己调用。每个 NFT(这里称为作品)有权获得铸造时买入的代币,以及按其铸造在曲线上花费的 SOL 比例分得的合集预留供应量。曲线完成后铸造的作品不在曲线上花费任何资金,因此既得不到代币,也没有份额。作品的份额按所选设置的计划解锁,从曲线完成的那一刻起计算,也就是其 Meteora 池开放前的那一刻。份额属于作品,而不属于铸造它的钱包:谁持有作品,谁就用 claim_dbc_piece 领取;GET /v1/dbc/pieces/<collection>/<serial> 会返回这个作品的份额已解锁多少、已领取多少。
流动性池
Meteora 曲线上的发行在 SOL 储备达到其配置设定的阈值时完成,标准配置下为 85 SOL。随后 Ripples 运行 migrate_damm_v2,按曲线达到的价格开放这个发行的 Meteora 池(DAMM v2),并永久锁定流动性;任何人都可以运行这一步,别人已经迁移的池会保持原样。此后,创作者在这个池中的锁定头寸持续收取手续费,用 cp-amm SDK 的 claimPositionFee 领取;曲线本身的创作者费可以随时用 claimCreatorTradingFee2 领取。交易终端从池的第一笔交易起就会收录它。
Ripples 程序上的发行按以下步骤转入 Raydium 池(CPMM):
beginGraduation 暂停代币交易,并记录用于建立流动性池的数量。createCpmmPool 准备所需的 SOL,并创建与这个发行绑定的 Raydium 恒定乘积池(CPMM)。池把这个发行的代币与发行自身的计价 mint 配对,使用该 mint 自己的代币程序。如果 mint 带有 Raydium 会审查的扩展,需要把它的 SupportMintAssociated 账户作为 remaining account 传入;没有这个账户的 mint 无法进入池。lockLp 锁定流动性提供者头寸。永久锁定使用 Raydium Burn & Earn。finalizeGraduation 在程序验证流动性头寸的存放位置之后,把转入标记为完成。池创建之前,发行的创作者或当前的 Ripples 程序权限方可以调用 resumeTrading,清除已准备的数量并重新开放代币交易。一旦记录了池头寸,这条恢复路径就会关闭。
限时锁定的流动性提供者头寸(LP 头寸)在 lpUnlockAt 之前一直留在程序所有的账户中。永久锁定使用 collectLockedFees:它把池两侧的手续费归集到发行自己的账户,并按发行的 creatorFeeShareBps 分配到账的金额,创作者的份额付给 creatorPayout,其余进入国库。它付款的 creatorPayout 与交易付款的是同一个地址,因此更改这个地址会同时改变两者。限时锁定的头寸只有在锁定到期后才能使用 withdrawUnlockedLp。
公开注册表 API
这个 API 服务于公开的网页应用。下列路由索引 Solana 链;传入 ?chain=robinhood 时,/v1/launches 也返回 Robinhood,/v1/launches/<address> 在两条链上都能查询。私有的 Virtuals 智能体集成是一项独立服务;这个 API 从不接收它的钱包凭证或交易请求。
| 方法 | 路径 | 用途 |
|---|---|---|
| GET | /v1/launches | 已索引的发行 PDA 及其当前公开状态。 |
| GET | /v1/collections | 已索引的合集 PDA 及其供应量。 |
| POST | /v1/auth/solana/challenge | 生成一条供钱包签名的一次性消息。 |
| POST | /v1/auth/solana/verify | 验证 Ed25519 钱包签名,并签发私有会话令牌。 |
| POST | /v1/launch-media | 校验并发布发行图片及其公开的代币或合集详情。需要 Solana 钱包会话。 |
| POST | /v1/collections/:collection/config/challenge | 将服务商配置绑定到创作者和合集。 |
| PUT | /v1/collections/:collection/config | 保存刚签名的服务商配置。 |
/v1/launch-media 接受 name、symbol、description,以及一个包含 MIME 类型和 base64 数据的 image 对象。可选的 socials 字段涵盖网站、X 和 Telegram;customLinks 最多接受 5 个带标签的 HTTPS 链接。图片格式为 PNG、JPEG、GIF、WebP 或 AVIF,大小不超过 1 MB。服务会核验文件的实际类型,把图片和 JSON 存放在按内容寻址的公开链接上,并返回 metadataUri 和 imageUrl。只要内容仍在托管,同一地址就始终返回相同的字节;Ripples 可以删除违反其条款的内容。浏览器会自动使用 metadataUri;使用标准表单的创作者无需自己填写或托管它。
地址采用 base58 编码,区分大小写。对挑战消息的 UTF-8 字节原样签名,并提交以 base58 编码的 64 字节原始 Ed25519 签名。这样无需密码或 Solana 交易,就能证明对钱包的控制权。每条挑战消息只能使用一次,并且会过期。
参考 · Robinhood
Ripples 由一组公开合约组成,源代码以 MIT 许可证发布在 github.com/ripples-dot-run/contracts。所有发行都由两个工厂合约部署,所以代币、它的市场、它的合集和它的代币释放合约,都来自你在使用之前就能阅读的代码。浏览器用 viem 调用这些合约,任何支持 Robinhood 的钱包都能审查并签名这些调用。中间没有任何环节能动用你的资金:发行和合集直接从工厂合约读取,你看到的成交下限,就是你的钱包签名的下限。
部署
approvedQuote(address) 和 quoteEconomics(address),用 quoteCount() 和 quotes(offset, limit) 分页linkOf(curve)。它只存储一个地址,不对发行方或该金融工具做任何验证contractURI。市场要通过创建它的那一组合约来读取和交易;市场页面会写明是哪一组。
NEXT_PUBLIC_LAUNCHPAD_CHAIN=rh-mainnet
NEXT_PUBLIC_LAUNCHPAD_RH_RPC_URL=https://rpc.mainnet.chain.robinhood.com端点为可选项,默认使用上面的地址。只有需要用你自己的源站代理公共端点时,才设置它。如果构建配置的链和端点分属不同的网络,应用会在启动时拒绝运行,而不会一边读取一个网络,一边标称自己是另一个网络。
指向主网的构建还需要设置 NEXT_PUBLIC_ALLOW_MAINNET=1,以防环境变量写错时,开发构建误连真实资金。
交易流程
每一笔扣款都通过授权额度拉取,而批准针对的是这次发行自己的计价资产,而不是工厂合约的默认资产:在以白名单资产计价的市场上买入或铸造,批准的就是那种资产,可通过市场或合集上的 QUOTE() 读取。交易是唯一不消耗普通授权额度的操作:它经过一个通过 Permit2 支出的路由合约,所以在某种资产上的第一笔交易,要先把代币批准给 Permit2 一次,再由 Permit2 按一定金额和有效期批准给路由合约。第二次授权指定的是实际下单的那个路由合约,授予一个路由合约的权限不覆盖经由另一个路由合约的交易。发行费是例外,始终以 ETH 收取(由 feeToken() 给出),所以创建以其他资产计价的发行需要两次批准:一次用于发行费,一次用于开盘买入。发行费和铸造在扣款的调用之前都需要一次 approve,而且都不接受原生币付款,所以用 ETH 支付时,会在同一个操作中先封装恰好所需的金额。在以 ETH 计价的市场上买入则完全不需要批准:它随兑换一起发送 ETH,详见下文的“代币交易”。应用会先规划好这两步,在向钱包发出任何请求之前用一次 eth_simulateV1 模拟,这样,铸造期已结束、触及钱包上限、发售已售罄或价格已变动,都会在尚未签名、也没有任何授权额度生效时暴露出来。节点无法模拟时,请求会在不模拟的情况下继续。
Gas 估算时会留出余量,价格取自 eth_feeHistory,节点不支持时回退到 eth_gasPrice。应用在发送前记录交易哈希和账户 nonce,所以交易确认中途刷新页面,会核对这笔交易,而不会再签第二笔。
兑换参数是这个网络与当前 v4-periphery 唯一不同的地方,而且出错时毫无提示。它的 SWAP_EXACT_IN_SINGLE 结构体在 amountOutMinimum 和指向 hookData 的偏移量之间多了一个头部字(head word)。如果按文档中的五字段结构体编码,路由合约会从那个未使用的槽位读取 hook data 偏移量。当资金池的第一种货币是链的原生资产时,误读落在一个零字上,兑换照常完成。当它是 ERC-20 时,同一个槽位存放的是代币地址,calldata 边界检查失败,调用在路由合约到达 PoolManager 之前就回滚,而且没有任何返回数据。每个以 ETH 计价的市场都以 ERC-20 计价,所以 encodeV4Swap 会写入第六个字段。
// One read prices the market: config, position, state, raise, price, opening tax.
// launchesOf lives on the lens; the hook has no such function and reverts.
const [view] = await client.readContract({
address: launchLens,
abi: LAUNCH_LENS_ABI,
functionName: "launchesOf",
args: [[poolId]],
});
// Which side the hook charges depends on the hook the pool is keyed to, so read
// view.config.hook before sizing the floor.
//
// On QuoteFeeHook every leg comes off the WETH side whichever side the swap
// specifies: the opening tax first, then the trade fee and the creator's own
// charge on what is left. An exact-input buy is charged on its input and the
// curve sees the net, so the floor is the constant-product move on that net.
const taxQ = (quoteIn * view.snipeTaxBps) / 10_000n;
const feeQ = ((quoteIn - taxQ) * view.config.tradeFeeBps) / 10_000n;
const creatorQ = ((quoteIn - taxQ) * view.config.creatorTaxBps) / 10_000n;
const netOnQuoteFee = tokensOut(view, quoteIn - taxQ - feeQ - creatorQ);
// On LaunchHook the cut comes out of the swap's unspecified side, which on an
// exact-input buy is the launch token: the opening tax first, then the trade fee
// and the creator's own charge on what is left.
const gross = tokensOut(view, quoteIn);
const tax = (gross * view.snipeTaxBps) / 10_000n;
const fee = ((gross - tax) * view.config.tradeFeeBps) / 10_000n;
const creatorTax = ((gross - tax) * view.config.creatorTaxBps) / 10_000n;
const net = gross - tax - fee - creatorTax;
// Size the floor on what reaches the buyer, at the tolerance the app signs.
// quoteFeeHook is the address the deployment table above lists.
const isQuoteFeeHook = (hook) => hook.toLowerCase() === quoteFeeHook.toLowerCase();
const reaches = isQuoteFeeHook(view.config.hook) ? netOnQuoteFee : net;
const amountOutMinimum = (reaches * 9_900n) / 10_000n;
// V4_SWAP is the router's one command; the actions inside it are
// SWAP_EXACT_IN_SINGLE, SETTLE_ALL, TAKE_ALL. The deadline is a wall-clock
// second: the router compares it to block.timestamp.
const { request } = await client.simulateContract({
account: buyer,
address: universalRouter,
abi: UNIVERSAL_ROUTER_ABI,
functionName: "execute",
args: [
"0x10",
[encodeV4Swap({ poolKey, zeroForOne: quoteIsCurrency0, amountIn: quoteIn, amountOutMinimum })],
BigInt(Math.floor(Date.now() / 1000) + 600),
],
});
const hash = await wallet.writeContract(request);
await client.waitForTransactionReceipt({ hash });代币发行
createLaunch 在一笔交易中部署 ERC-20 代币和这次发行的锁仓合约,开设资金池并建立曲线头寸。全部供应量在创建时一次性铸出,此后不存在任何铸币权限。createLinkedLaunch 把合集和代币释放合约也放进同一笔交易,因此组合发行只需一笔交易,不会留下需要修复的半成品状态。两个调用都从调用方的授权额度中扣取发行费,所以钱包要在调用之前先批准这笔支出。
两个调用都可以附带一笔开盘买入。工厂合约通过这次发行自己的 LPLocker 执行这笔买入,并把它登记为豁免地址,所以这笔买入只需支付交易手续费。豁免名单有三个写入来源:登记时的创作者、通过 snipeExempt 传入的每个地址,以及之后用 setCreatorFeeRecipient 改设的手续费收款地址。实际豁免的范围比字面上窄。Hook 合约用豁免名单比对的是调用 PoolManager 的地址,而每笔普通交易都经由路由合约到达 PoolManager,所以创作者之后再买入,也和其他买家一样要付开盘税。通过 snipeExempt 传入的地址,只有在自己直接向 PoolManager 发送买入时才享受豁免。
这两个调用对应两种发行形态。单独发行以较小的定价储备开盘,因为它的募集资金只能随交易逐笔进入,而每笔交易都会推动价格。组合发行以较大的定价储备开盘,因为 NFT 合集可以把整笔募集资金导入,价格完全不动。两者的目标金额相同,注册表中对应的那一行为每种形态各记录了一个储备值。
const params = {
name: "Ripple",
symbol: "RPL",
curveSupply: 800_000_000n * 10n ** 18n,
lpTokenSupply: 219_000_000n * 10n ** 18n,
vQuoteInit: 275_880_000_000_000_000n,
vTokenInit: 1_073_000_000n * 10n ** 18n,
graduationQuote: 689_700_000_000_000_000n,
lpUnlockAt: 0n,
// On quoteFeeFactory the charge below is the whole of what trading pays the
// creator: that factory stamps no creator share into the trade fee.
creatorTaxBps: 0n, // the creator's own charge, 0 to 1000 bps
// The asset this market settles in. The zero address means the factory's default.
// vQuoteInit and graduationQuote above are in THIS asset's own decimals and must
// equal the row in the registry that belongs to the factory you create on, so
// read them from quoteFeeRegistry.quoteEconomics(quote) rather than typing them.
// The two factories keep different rows for the same asset and each refuses the
// other's.
quote: approvedQuote,
};
// The launch fee is charged in feeToken(), so it is approved, not sent as value. A
// launch priced in another approved asset approves that asset for the opening buy as
// well, which is two permissions rather than one.
await wallet.writeContract({
address: feeToken,
abi: QUOTE_ABI,
functionName: "approve",
args: [quoteFeeFactory, launchFee],
});
const { request } = await client.simulateContract({
account: creator,
address: quoteFeeFactory,
abi: TOKEN_FACTORY_ABI,
functionName: "createLaunch",
args: [params],
});交易手续费不是发行参数。这个工厂合约开设的每个市场都按 TRADE_FEE_BPS 收费,池手续费和 tick 间距则是 Hook 合约自身的常量。上面的储备值是单独发行的那一组。createLinkedLaunch 声明的是组合发行的那一组;任何一种形态如果声明了另一种形态的储备值,工厂合约都会拒绝。
lpUnlockAt 为零,是工厂合约表示流动性永不解锁的方式。如果某个配置的供应量不足以支撑自身的毕业、初始价格超出允许区间,或者创作者税超过 MAX_CREATOR_TAX_BPS,工厂合约都会拒绝,所以无法发行的市场根本不会上链。
NFT 合集
Robinhood 上的合集采用标准的 ERC-721 和 ERC-7572 接口,并支持 ERC-2981 版税,因此索引这条链的交易市场都可以上架它们。这项支持目前的状态,请到 OpenSea 查看。只要这条链上有对应的交易市场地址,合集页面和这里的每一行 NFT 都会链接到该合集在交易市场上的地址。
上述工厂合约部署的合集会响应 contractURI()(ERC-7572)。工厂合约在创建时把它指向 https://api.ripples.run/v1/collection-assets/<collection address>/contract.json,这份文档包含合集名称、一张方形 logo、合集在本站的页面,以及从合约读取的版税比例和收款地址,所以交易市场的页面无需任何人认领就会自动填好。交易市场显示的简介来自创作者发布的作品图,而不是这份文档。合集只读取一次源地址并一直保留,所以在工厂合约发布源地址之前创建的发售,这里不会返回任何内容;合集页面会写明它属于哪一种。自行托管文档的创作者可以随时用 setContractURI 重新指向,作品图冻结之后也可以。
createCollection 以固定价格部署合集,价格以合集自身的计价资产计;除非创作者在创建时指定了其他白名单资产,否则就是 ETH。指定资产后,这种资产在注册表中的那一行就具有约束力:价格必须不低于这一行的下限 minPriceQuote,注册表未列入白名单的资产一律拒绝。未指定资产的合集使用工厂合约的默认资产,不受任何一行约束。价格规则的另一半,即每次铸造导入市场的份额在该资产的小数精度下不能舍入为零,要在两个数字都存在的地方检查:createLinkedLaunch 在创建时检查一次,Collection721.linkToCurve 在两者关联时再检查一次。组合发行的合集和曲线共用同一种资产,所以铸造时导入的份额会以铸造者支付的同一种资产进入市场;如果两者资产不一致,linkToCurve 会以 QuoteMismatch 回滚,而不会把它们关联起来。预生成合集立即从 baseURI 提供作品图。实时生成的合集在平台签名者为每件作品写入一次最终 URI 之前,提供的是 placeholderURI。mint 每笔交易最多铸造 20 个 NFT,同时受合集的每个钱包上限约束,并在同一次调用中把付款中联动的那部分导入代币市场。
| 调用 | 调用方 | 用途 |
|---|---|---|
mint(qty, minRoutedTotal) | 铸造者 | 支付合集的计价资产并铸造 NFT,同时为导入份额设定下限。 |
setTokenURI | 平台签名者 | 替换实时合集的占位图,只能替换一次。 |
setBaseURI | 创作者 | 在冻结之前,更改预生成合集读取作品图的链接。 |
setPlaceholderURI | 创作者 | 在冻结之前更改实时合集的占位图。 |
setContractURI | 创作者 | 重新指向交易市场读取的合集文档。不受冻结影响,冻结只针对作品图链接。 |
freezeMetadata | 创作者 | 永久锁定作品图链接,之后无法再更改。 |
creatorWithdraw | 创作者 | 只提取已记入创作者名下的销售收入。 |
在铸造之外转入合集的计价代币不计为付款。任何人都可以调用 recoverUnattributed,它只会把这部分多余资金发给记录在案的创作者。
代币交易
Robinhood 上的市场就是一个挂载了 Ripples Hook 合约的普通 Uniswap v4 池。买入或卖出都是通过 Universal Router(0x8876789976dEcBfCbBbe364623C63652db8C0904)执行的一次 v4 兑换,经由 Permit2(0x000000000022D473030F116dDEE9F6B43aC78BA3)支出,所以任何已经支持 v4 路由的聚合器都能交易 Ripples 市场,无需集成 Ripples 的任何组件。这条路径能处理市场可以全额成交的任何订单,也就是所有常规规模的订单。应用所说的市场地址,是这次发行的 LPLocker。它持有头寸,提供这次发行自身的各项信息,但不接受订单。
如果订单超过市场剩余可卖的数量,就要走另一条路径。一次发行的流动性是覆盖一段有界价格区间的单个头寸,LaunchHook 会拒绝任何结束点落在区间之外的兑换。Universal Router 下单时使用很宽的价格限制,所以超量的买入会越过区间终点,遭到拒绝并整体回滚。Ripples 自己的路由合约(0x22DB9B2c6C6DB56ABC3fc196E4bC1FBb9c4338a6)则以区间终点作为价格限制来执行同一笔兑换。资金池成交到曲线终点即停止,用不到的计价资产也不会从钱包中扣走。当买入用 WETH 支付,或者市场以其他资产计价时,应用走的就是这条路径;市场达到目标金额前的最后一笔买入,也应该走这条路径。挂载 QuoteFeeHook 的市场在 0x0e558e1e65BD1850730A3B4a89654c066fC035B7 接受同样的调用:v4 池的 Hook 是池键的一部分,所以针对一个 Hook 构建的路由合约会拒绝另一个 Hook 下的所有资金池,市场页面会写明该用哪一个。
swapExactIn(key, zeroForOne, amountIn, minAmountOut, recipient, deadline) 返回 (spent, amountOut),和 Universal Router 路径一样通过 Permit2 授权额度支出,只是授权给的是这个路由合约而不是那个。spent 是资金池实际消耗的数量,最多等于 amountIn。minAmountOut 表示的是价格而不是数量:路由合约要求 spent * minAmountOut <= amountIn * amountOut,对于全额支出的订单,这正好等于 amountOut >= minAmountOut;对于部分成交的订单,则要求已成交部分达到同样的兑换率。因此,按整笔订单签名的下限,无论订单最终如何成交,含义都不变。
成交结果从 Filled(poolId, sender, recipient, zeroForOne, amountIn, spent, amountOut) 读取。spent 小于 amountIn 就是部分成交,差额是留在发送方手中的资金。PoolManager 自己的 Swap 只报告已成交的部分,对未成交的部分只字不提,所以只有把 amountIn 和 spent 放在一起,才能看出部分成交。市场已无可卖数量时,回滚错误是 CurveEndReached。
以 ETH 计价的市场以 WETH 结算,应用通过 Universal Router 用 ETH 交易,买卖各一次调用。买入时把 ETH 作为调用的 value 发送:先用 WRAP_ETH 存入路由合约自身的余额,再执行 V4_SWAP,由路由合约 SETTLE 付款、TAKE_ALL 给买家,最后用 UNWRAP_WETH 退回兑换剩下的部分,全程无需批准。卖出时执行 V4_SWAP,把 WETH 转给路由合约(TAKE 给路由合约自己),再以下限作为最小值执行 UNWRAP_WETH 给卖家,所以下限约束的是最终到账的 ETH。两个方向都不会把 WETH 留在路由合约或交易者手中。
池键由 LaunchHook.poolKeyFor(token, quote) 给出,它的 id 就是 PoolManager 使用的同一个哈希,所以无需先读取任何数据就能定位一个市场。poolIdOfToken(token) 则反过来,从代币查到池 id。
定价所需的一切都来自一次读取:上方部署表中市场读取合约上的 LaunchLens.launchesOf(bytes32[] poolIds)。Hook 合约本身没有 launchesOf,向它发送这个调用会回滚。每个 LaunchView 包含配置、初始注资方案、实时状态、已募集金额、池价格,以及读取时所在区块生效的开盘税。手续费读 config.tradeFeeBps,目标金额读 config.graduationQuote,计价资产读 config.quote,开盘税读 snipeTaxBps。开盘税从 config.launchedAt 开始递减,这是交易开启的时间,而不是发行登记的时间。计价资产的小数位数要从计价代币本身读取:发行中的任何合约都不会再次公布这个值,按错误的小数位数读取目标金额,会差出好几个数量级。Lens 合约不保存状态,也没有任何权限,它返回的每个值都是从 Hook 合约自身的视图函数和 PoolManager 读回来的,所以不信任它的调用方可以从这两个地址复现每个字段。
给市场定价要依据头寸和池价格,不要依据资金池的活跃流动性,曲线头寸完整时它读出来是零。计算下限之前先读 view.config.hook:Hook 合约从兑换的哪一侧收费,取决于 Hook 本身,下面两个 Hook 的做法不同。还原一笔成交,要把 PoolManager 的 Swap 与 TradeFeeCharged、CreatorTaxCharged 和 SnipeTaxCharged 结合起来看:只看 Swap,交易者实际收到的数量会多算出 Hook 收走的全部金额。
网页表单创建的 Robinhood 市场,交易手续费为 1%,这就是全部手续费;市场会一直保留登记时的手续费、分成比例和 Hook 合约。交易开启后的前 3 秒内,买入还要额外支付开盘税,税率从 99% 开始递减至零;先收开盘税,再对剩余部分收取交易手续费和这次发行自己的 config.creatorTaxBps。创作者税全额记给创作者,不参与任何分成,所以设置了创作者税的市场,从一笔买入中扣取的是三项而不是两项。
由 QuoteFeeFactory 0x66112C2c987eD7A366715bD28CCc0DF629813206 创建的市场挂载 QuoteFeeHook 0x6172fD469E900F74eD04b1763daB4D2c2BFB6AcC,它从兑换的 ETH 一侧扣取每一项费用:该侧为指定侧时在 beforeSwap 中扣取,不是指定侧时在 afterSwap 中扣取。因此买入和卖出都以 ETH 付费,开盘税从买家支付的计价资产中扣除,而不是从买家收到的代币中扣除。Hook 合约把手续费和开盘税记入工厂合约指定的国库,即 Ripples 国库。2026年11月1日之前,这个国库是 QuoteFeeRouter 0xC8842e8196E96Cef0988Cffb144722fDe8a8a684,它把 70% 发给回购销毁合约,30% 发给国库。这个工厂合约创建的每次发行,config.creatorFeeBps 都是 0。Founders Window 期间创建的市场是有意设计的例外:它的工厂合约(0x1C4032F557621D3b77E11748706BEfd26F3b6ef4)在 config.creatorFeeBps 中写入 30%,从第一笔交易起在市场存续期间一直支付给创作者,其余手续费归 Ripples 国库。在 Founders Window 开放期间于 Ripples 发行,并在 7 天内市值达到 $30K,我们就为你支付 DEX Screener 的 Enhanced Token Info 费用。面向前 10 个符合条件的项目,可与现有的 Founders Window 权益同时享有。config.creatorTaxBps 以 ETH 按同一基数收取,全额记给创作者。
由 TokenLaunchFactory 0xE89C1b33B6C2505414667bE7E5084774a72150e1 创建的市场(包括 $RIPPLES 和 $KOI)挂载 LaunchHook 0xaF59944A7d03B914567cb0272b7E588A7aE7AAC4,它从兑换的非指定侧扣取费用,对于精确输入的买入,这一侧就是发行的代币,所以你签名的下限要按扣费后的净额计算,而不是按资金池付出的数量计算。从这次发行的第一笔交易起,它就在 afterSwap 中按 config.creatorFeeBps 拆分手续费,并以手续费实际落入的币种,把双方的份额记入自己的提取式托管账本:买入的手续费以发行代币计,卖出的以计价资产计。分成比例要从这次发行本身读取,不要自行假设。普通买入时,税款以发行代币计,Hook 合约把它记入 BURN_SINK,买家支付的计价资产则留在市场中。
兑换过程中不会有任何资金离开 Hook 合约。它欠某个地址的金额由 owed(token, account) 给出,用 claim(token) 领取。claimFor(token, account) 是任何人都可以发起的同一笔支付,而且只会付给 account。创作者可以用 setCreatorFeeRecipient(poolId, recipient) 把这笔收入转到别处,这个调用只能由当前的收款地址发起,最初就是创作者本人。
| 调用 | 用途 |
|---|---|
LaunchLens.launchesOf(poolIds) | 按池 id 返回配置、头寸、状态、已募集金额、价格和开盘税。 |
LaunchHook.snipeTaxBps(poolId) | 当前区块生效的开盘税,以基点计。 |
LaunchHook.snipeExempt(poolId, account) | 某个地址是否免付开盘税。比对的是 PoolManager 的调用方,所以经由路由合约交易时,路由合约会把交易者挡在身后。 |
LPLocker.QUOTE() · LPLocker.TOKEN() | 这个市场配对的两种资产。 |
LPLocker.realQuote() · GRADUATION_QUOTE() | 已募集金额和毕业所需的募集金额,以计价资产自身的单位计。 |
LPLocker.graduated() | 募集是否已结束。 |
LaunchHook.owed(token, account) | Hook 合约在某种资产上仍欠某个地址的金额,以该资产自身的单位计。 |
这一代之前创建的市场是联合曲线,而不是资金池。它们带有 buy、buyFor、sell、sellWithPermit、quoteBuyFor、TRADE_FEE_BPS、QUOTE_DECIMALS() 和 LAUNCH_START。当前工厂合约创建的市场上没有这些选择器,调用任何一个都会回滚。要区分两者,就在市场地址上调用 poolId():基于资金池的市场会响应,曲线市场不会。
组合发行
曲线开放期间,每笔 NFT 付款中按 mintToCurveBps 计算的部分进入曲线储备,并在 AllocationVesting 中记在铸造者名下。导入的计价资产计入目标金额,但不改变决定价格的虚拟储备。
铸造者的份额在发行毕业、预留部分交付时确定:allocationOf(minter) 按其导入的金额按比例计算。claimable(minter) 返回已释放的数量,claim() 按这次发行的锁定期和释放期限发放。领取权属于铸造时使用的钱包,卖出或转让 NFT 不会转移它。受益人全部领取完毕后,finalizeBeneficiary(minter) 会结清该受益人,这样 burnSurplus 就能销毁预留余额之外的舍入零头。
智能体发行
AgentTreasuryFactory.createAndLaunch(charter, tp, np, lp, d, runway) 部署一个 AgentTreasury(智能体国库),从调用方转入 runway,再由它用组合发行所需的同一组元组调用 TokenLaunchFactory.createLinkedLaunch。因此,这个国库就是这次发行的创作者,每笔交易手续费中的创作者分成都记给它。先向工厂合约批准数额为 runway 的 RUNWAY_ASSET();它是发行工厂合约的手续费代币,以其他资产结算的发行一律拒绝。
章程是 (address operator, uint128 dailySpend, uint128 perCallSpend, uint128 dailySell, uint32 dailyMint, address[] payees),在构造函数中固定。三个计价金额以 RUNWAY_ASSET 计,dailySell 以发行代币计。perCallSpend 必须大于零且不超过 dailySpend;收款方最多八个,每个只能出现一次。
操作者密钥可以调用 buy(amountIn, minAmountOut, deadline)、sell(amountIn, minAmountOut, deadline)、mint(quantity, minRoutedTotal)、pay(to, amount) 和 note(digest, uri)。交易只走这次发行自己的资金池;国库从自己的锁仓合约读取池键,所以没有交易场所参数。离开合约的计价资产,在买入、铸造和支付中合并计入 dailySpend 和 perCallSpend;卖出按下单数量计量,而不是按成交数量。计量窗口为 UTC 自然日。remainingToday() 返回每项上限的剩余额度。
claimFees() 和 claimVested() 均无需许可,且只能向国库付款:前者从 Hook 合约、锁仓合约和合集中提取创作者分成,后者提取国库自己铸造所获得的那部分代币。创作者保留 setCollectionBaseURI、setCollectionPlaceholderURI 和 freezeCollectionMetadata,除此之外没有任何调用权限。合约没有所有者,不能暂停,也不能提款。
每个调用都会发出事件:Launched、Bought、Sold、Minted、Paid、FeesClaimed、VestedClaimed 和 Noted。要把这个工厂合约部署的国库与其他响应相同读取调用的合约区分开,就用 AgentTreasuryFactory.isFromFactory(address)。
@ripplesdotrun/agent-sdk 用你的智能体自己的密钥为这些调用签名,@ripplesdotrun/mcp 通过 stdio 把它们提供给模型。两者都不负责运行智能体:模型循环、记忆和策略仍由你掌控。运营资金、各项上限和作品价格都以 RUNWAY_ASSET() 计,与上文相同。
import { createRipplesAgent } from "@ripplesdotrun/agent-sdk";
import { privateKeyToAccount } from "viem/accounts";
const agent = createRipplesAgent({
network: "mainnet",
account: privateKeyToAccount(process.env.RIPPLES_PRIVATE_KEY),
allowMainnet: true,
});
const { treasury, token, market } = await agent.launch({
runway: "0.05",
charter: {
operator: process.env.OPERATOR_ADDRESS, // a key of its own, apart from this wallet
dailySpend: "0.03",
perCallSpend: "0.01",
dailySell: "100000",
dailyMint: 3,
payees: [process.env.PAYEE_ADDRESS],
},
token: { name: "Field Notes", symbol: "NOTES", image: "https://example.com/notes.png" },
collection: { name: "Field Notes Pieces", symbol: "NOTESP", price: "0.01", supply: 88, baseUri: "https://example.com/notes/" },
});
console.log(treasury, token, market);{
"mcpServers": {
"ripples": {
"command": "npx",
"args": ["-y", "@ripplesdotrun/mcp"],
"env": {
"RIPPLES_NETWORK": "mainnet",
"RIPPLES_ALLOW_MAINNET": "1",
"RIPPLES_MAX_PER_CALL": "0.02",
"RIPPLES_PRIVATE_KEY": "…"
}
}
}
}Wake
Wake 是指把一个市场的创作者收入导向一个专为某个 NFT 创建的合约。AgentPayeeFactory.payeeOf(nft, tokenId) 在这个合约存在之前就能返回它的地址,create(nft, tokenId) 负责部署。两者任何人都可以调用;再次 create 会返回同一个地址;链上还没有任何地址持有这个 NFT 时,create 会以 NoHolder() 回滚。随后,由市场当前的创作者费收款地址调用 Hook 合约的 setCreatorFeeRecipient(poolId, payee)。只有当前收款地址能更改去向,而收款合约没有任何能再次更改它的调用,所以这条路由是永久的。
AgentPayee.release(hook, currency) 负责支付,任何人都可以调用。它从 Hook 合约中提取为这个收款合约保管的 currency(ETH 用零地址表示),再把收款合约的全部余额发给 holder(),后者读取的是当时的 ownerOf。NFT 售出时,收款合约本身没有任何变化:下一次支付会找到新的持有者。每次支付都会发出 Released(currency, to, amount)。
由多件作品共享的 Wake 则导向一个 WakeSplit。WakeSplitFactory.splitOf(pieces) 和 create(pieces) 接受 1 至 256 个 (address nft, uint256 tokenId, uint32 weight) 条目,先按 NFT、再按 token id 排序,不得重复,权重不得为零,而且创建时每件作品都必须有持有者。每件作品获得到账金额的 weight / totalWeight,向下取整。release(hook, currency, index) 支付一件作品;releaseMany(hook, currency, indexes) 在一次提取后支付多件,遇到无法支付的作品时发出 ReleaseFailed 并跳过,而不是回滚。读取用 due(currency, index)、holderOf(index) 和 pieces()。发行表单给每件作品相同的权重;合约本身接受任意权重。
在 ripples.run 上,留下 Wake 的同时也会把这件作品注册为 ERC-8004 智能体。在链上,这是一个单独的调用:持有者通过 ERC-8217 绑定适配器调用 register(0, nft, tokenId, agentURI) 注册作品,其中 0 表示 ERC-721,此后这个智能体由 NFT 的当前持有者控制。这些合约都没有所有者,也没有任何可调整的设置。
API 提供同样的读取,对任何来源开放。GET /v1/wake?chain=robinhood 列出最多 500 个 Wake 及最近 50 次支付。GET /v1/wake/{rail}/{collection}/{tokenId} 返回单件作品,GET /v1/wake/by-market/{rail}/{token} 返回某个市场所支付的 Wake,GET /v1/wake/split/{rail}/{split} 返回一个共享 Wake 及其所有组成部分。每个 Wake 包含 payee、piece、当前的 holder、向它付款的 markets、owed(现在支付会发出的金额)、paid、lastPayout,共享 Wake 的组成部分还带有 share。金额是以最小单位表示的十进制字符串,ETH 用零地址表示。
智能体的读取方式相同:GET /v1/agents?wallet=0x…、GET /v1/agents/{chainId}/{agentId}、用于解析 8004scan 或 OpenSea 链接的 GET /v1/agents/resolve?link=,以及返回作品 ERC-8004 注册文件的 GET /v1/pieces/{rail}/{collection}/{tokenId}/agent.json。rail 取值为 robinhood。
import { createWakeClient } from "@ripplesdotrun/agent-sdk";
import { privateKeyToAccount } from "viem/accounts";
const wake = createWakeClient({
chain: "robinhood",
account: privateKeyToAccount(process.env.RIPPLES_PRIVATE_KEY),
allowMainnet: true,
});
// Route a market you created to one piece, read it, and pay whoever holds it.
await wake.pinWake({ token, nft, tokenId });
console.log(await wake.wake({ nft, tokenId }));
await wake.releaseWake({ nft, tokenId });
// Or share it across pieces, then pay every part.
const { split } = await wake.shareWake({ token, pieces: [{ nft, tokenId: 1 }, { nft, tokenId: 2 }] });
await wake.releaseSplit({ split });bindToken 把你持有的作品注册为智能体,splitOf 和 wakeOfSplit 读取共享 Wake。MCP 服务器以 ripples_bind_token、ripples_pin_wake、ripples_wake、ripples_release_wake、ripples_share_wake、ripples_wake_split 和 ripples_release_split 提供同样的调用;需要签名的调用要设置 RIPPLES_PRIVATE_KEY 和 RIPPLES_ALLOW_MAINNET=1,路由操作还要求传入 acknowledge: "permanent",因为它无法撤销。
Uniswap v4 流动性池
当 LPLocker.realQuote() 达到 GRADUATION_QUOTE() 且开盘税期已结束,LaunchHook.graduationReady(poolId) 就会变为 true。此后任何人都可以调用 LPLocker.settleGraduation(),且它只会执行一次。它在 Hook 合约上结束募集,整体退出曲线头寸,用募集所得和联动合集导入的资金开设一个全区间头寸,把预留部分交给联动发行,并销毁剩余的全部供应量。从募集结束到永久头寸建立之间,兑换一律拒绝,所以这些步骤在一笔交易中完成,而不是分成两笔。超出代币库存可配对部分的计价资产会转入国库,而不会改变开盘价格。
市场地址上没有 graduate()。Hook 合约的 graduate(poolId) 只接受这次发行自己的锁仓合约调用,所以应该调用的是 settleGraduation():它无需许可,任何人都可以完成一个已达到目标金额的市场,而且它在一笔交易中结束募集并结算头寸,否则整体回滚。
锁仓合约按 UNLOCK_AT 持有头寸。永久锁定永远走不到 unlock。资金池本身不收手续费,所以 collectFees 通常什么也收不到。保留它是因为头寸真实存在,资金池一旦产生手续费,就没有其他办法分配。这个调用按工厂合约写入锁仓合约的分成比例拆分头寸手续费:来自 QuoteFeeFactory 0x66112C2c987eD7A366715bD28CCc0DF629813206 的锁仓合约不给创作者分成,头寸手续费全部发往工厂合约记录的国库;来自 TokenLaunchFactory 0xE89C1b33B6C2505414667bE7E5084774a72150e1 的锁仓合约,70% 归创作者,其余归国库。毕业后的市场付给创作者的,是 Hook 合约记入的交易手续费,用 LaunchHook.claim 领取。有时间锁的头寸在到期时间之后可以解除,并对两种资产的数量都设有下限。
读取发行列表
每次发行都从创建它的工厂合约读取。这条链上有两个代币发行工厂合约创建过市场,所以完整的发行列表要分页读取两者,并依次向每一个调用 isFromFactory。launchCount() 和 launches(offset, limit) 分页读取代币发行;collectionCount() 和 collections(offset, limit) 分页读取合集发售。isFromFactory(address) 用来确认一个地址来自 Ripples,而不是来自仿冒部署:代币一侧传入发行的锁仓合约,NFT 一侧传入合集地址。Multicall3 部署在 Robinhood 的标准地址上,所以一页发行只需一次请求就能读完。
const count = await client.readContract({
address: tokenLaunchFactory,
abi: TOKEN_FACTORY_ABI,
functionName: "launchCount",
});
const page = await client.readContract({
address: tokenLaunchFactory,
abi: TOKEN_FACTORY_ABI,
functionName: "launches",
args: [0n, count],
});
// page: { token, locker, hook, creator }[]代币自己就能回答这些信息。name() 和 symbol() 是 ERC-20 的那一对字段;logo() 和 description() 返回字符串;socials() 依次返回 twitter、telegram、discord、website 和 farcaster 五个链接,其中任何一个都可能为空。这七项都在代币的构造函数中写入,没有 setter,所以你读到的就是发行时冻结的内容。发行的任何信息都不依赖这个应用控制的索引。
你自己的智能体
已经持有钱包的智能体从公共 API 读取发行,从部署表中的端点或自己的节点读取链上状态,构建与浏览器相同的调用,并用自己的密钥签名。所有网络费和交易都由这个钱包支付。本页的任何内容都不会代它签名、付款或批准。
GET https://api.ripples.run/v1/launches?chain=robinhood 按从新到旧的顺序列出 Robinhood 上的代币发行,返回 count、nextOffset 和 launches 数组。每一行包含 address(代币)、pair(其市场交易所在的池 id)、creator、带名称和代币符号的 token,以及 status。/v1/launches/<address> 按代币地址或池 id 返回单次发行及其当前市场状态。也可以直接从工厂合约读取发行,发行列表就是这样做的。
第一次发行出现之前,列表返回 count: 0,所以循环要能处理空页。limit 默认为 50,最大为 200;offset 从最新的一行开始计数。每个客户端地址每分钟最多读取 120 次,每个响应都会在 x-ratelimit-remaining 中给出剩余次数。如果想在测试网上运行同样的调用,把 RIPPLES_API 指向你自己构建的 API。
在 Robinhood 上,市场是一个 Uniswap v4 池,所以智能体用 viem 从 Hook 合约读取价格,并通过 Universal Router 兑换,所用地址来自 Robinhood 部署表。路由合约经由 Permit2 支出,所以要先把计价代币批准给 Permit2,再由 Permit2 批准给路由合约。网络费以 ETH 从同一个钱包支付。
import { createPublicClient, createWalletClient, http, parseAbi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
const chain = {
id: 4663,
name: "Robinhood",
nativeCurrency: { name: "ETH", symbol: "ETH", decimals: 18 },
rpcUrls: { default: { http: ["https://rpc.mainnet.chain.robinhood.com"] } },
};
const account = privateKeyToAccount(agentPrivateKey);
const client = createPublicClient({ chain, transport: http() });
const wallet = createWalletClient({ account, chain, transport: http() });
const QUOTE = parseAbi(["function approve(address spender,uint256 value) returns (bool)"]);
const PERMIT2_ABI = parseAbi([
"function approve(address token,address spender,uint160 amount,uint48 expiration)",
]);
const ROUTER = parseAbi([
"function execute(bytes commands,bytes[] inputs,uint256 deadline) payable",
]);
// Price the market off LaunchLens.launchesOf and take the hook's cut off the output
// before signing a floor. The Robinhood "Token trading" section shows that
// arithmetic in full, and where the lens is deployed.
const quoteIn = 10n ** 16n;
const amountOutMinimum = floorFor(quoteIn);
const expiry = Math.floor(Date.now() / 1000) + 3_600;
await wallet.writeContract({ address: quoteToken, abi: QUOTE, functionName: "approve", args: [permit2, quoteIn] });
await wallet.writeContract({ address: permit2, abi: PERMIT2_ABI, functionName: "approve", args: [quoteToken, universalRouter, quoteIn, expiry] });
const hash = await wallet.writeContract({
address: universalRouter,
abi: ROUTER,
functionName: "execute",
args: [
"0x10",
[encodeV4Swap({ poolKey, zeroForOne: quoteIsCurrency0, amountIn: quoteIn, amountOutMinimum })],
BigInt(expiry),
],
});
await client.waitForTransactionReceipt({ hash });