简介
实体用户是一种 Clarity 原生类型,表示可以拥有一定代币余额的实体。本节讨论 stacks 区块链实体用户及其在 Clarity 语言中的使用方法。
stacks 区块链实体用户和 tx-sender
智能合约语言和区块链中的资产由 stacks 区块链实体用户类型的对象“拥有”,这意味着实体用户类型的任何对象都可以拥有资产。拿公钥哈希和多人签名的 STX 地址举例,一个指定的实体用户可以通过在区块链上发布已经签名的交易来对其资产进行操作。一个智能合约程序也可能是一个实体用户(由智能合约的标识符表示),但是,没有与智能合约相关联的私钥,它不能在 stacks 区块链上广播已签名的交易。
Clarity 智能合约程序可以使用全局定义的 tx-sender 变量来获取当前实体用户。以下案例定义了一种交易类型,如果金额是 10 的倍数,则将 amount 变量的 microSTX 金额从发送方转移到接收方,否则返回 一个 400 错误。
(define-public (transfer-to-recipient! (recipient principal) (amount uint))
(if (is-eq (mod amount 10) 0)
(stx-transfer? amount tx-sender recipient)
(err u400)))
clarity 语言提供一个额外的变量,来帮助智能合约程序授权一个交易发送者。关键字 contract-caller 返回调用当前合约的实体用户。如果发生合约程序之间调用,contract-caller 会返回调用者堆栈中的最后一个合约。
例如,假设有三个合约 A 、B 和 C,每个合约都有一个 invoke 函数,比如 A::invoke 调用 B::invoke 和 B::invoke 调用 C::invoke 。
当用户 Bob 发出一个交易时,调用 A::invoke,每个连续调用函数中的 contract-caller 的值都会改变:
in A::invoke, contract-caller = Bob
in B::invoke, contract-caller = A
in C::invoke, contract-caller = B
这允许合约不仅使用 tx-sender (在本例中,始终是“Bob”),而且还使用 contract-caller,允许智能合约程序进行断言和执行授权检查。可以用于确保仅仅只能够直接调用特定函数,而从不通过合约程序之间调用(通过断言 tx-sender 和 contract-caller 相等)。我们在下面的火箭飞船示例中,提供了两种不同类型的授权检查的例子。
智能合约本身作为实体用户
智能合约本身就是一个实体用户,可以由智能合约的标识符表示——即合约程序的发布地址和合约名称,例如:
'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR.contract-name
为方便起见,智能合约可能会以 .contract-name 的形式编写合约的标识符。这将由 Clarity 解释器扩展为一个完全合格的合约标识符,该标识符对应于与其出现的合约相同的发布地址。 例如,如果相同的发布者地址 SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 正在发布两个合约程序,contract-A 和 contract- B,合约程序合格的标识符将是:
'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR.contract-A
'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR.contract-B
但是,在合约程序的源代码中,如果开发者希望从 contract-B 中的 contract-A 调用一个函数,他们可以这样写:
(contract-call? .contract-A public-function-foo)
这允许智能合约开发人员在不知道发布 key 的情况下将他们的应用程序模块化到多个智能合约中。
为了让智能合约本身对其拥有的资产进行操作,智能合约可以使用特殊的( as-contract ...)函数。 此函数执行表达式(作为参数传递),将 tx-sender 设置为合约程序的实体用户,而不是当前的发送者。as-contract 函数返回提供的表达式的值。
例如,一个实现类似“代币水龙头”的智能合约程序可以这样写:
(define-map claimed-before
((sender principal))
((claimed bool)))
(define-constant err-already-claimed u1)
(define-constant err-faucet-empty u2)
(define-constant stx-amount u1)
(define-public (claim-from-faucet)
(let ((requester tx-sender)) ;; set a local variable requester = tx-sender
(asserts! (is-none (map-get? claimed-before {sender: requester})) (err err-already-claimed))
(unwrap! (as-contract (stx-transfer? stx-amount tx-sender requester)) (err err-faucet-empty))
(map-set claimed-before {sender: requester} {claimed: true})
(ok stx-amount)))
在此示例中,公共函数 claim-from-faucet:
检查发送者之前是否已从“水龙头程序”认领过代币。
将 tx sender 分配给 requester 变量。
向跟踪地图添加一个条目。
使用 as-contract 发送 1 个 microstack
与其他实体用户不同,该智能合约程序没有关联的私钥。由于缺少私钥,Clarity 智能合约无法在区块链上广播已签名的交易。
例子:授权检查
tx-sender 、contract-caller 和 as-contract 之间的交互是微妙的,但在合约程序中执行授权检查时很重要。在这个示例合约程序中,我们将展示合约程序可能希望执行的两种不同类型的授权检查,然后学习调用合约程序函数的不同方式,合约程序的函数将如何通过这些检查,或者如何执行失败。
该合约程序定义了一种“火箭飞船”不可替代的代币,实体用户可以拥有和管理授权飞行员的权限。飞行员也是被允许“驾驶”火箭飞船的实体用户。
该合约程序执行两种不同的授权检查:
在允许飞船飞行之前,合约程序会检查这笔交易是否由授权的飞行员创建和签署。例如,飞行员可以调用另一个合约程序,然后该合约程序以飞行员的名义调用 fly-ship 的公有函数。
在修改飞船的允许飞行员之前,合约程序会检查交易是否由飞船的所有者签署。此外,合约程序要求该函数由飞船拥有人直接调用,而不是通过 inter-contract-call 调用。
第二种检查比第一种检查更严格,有助于保护非常敏感的程序执行——它保护用户不会在不知不觉中调用恶意合约程序上的函数,这些恶意合约程序会试图调用另一个合约程序上的敏感函数。
;;
;; rockets-base.clar
;;
(define-non-fungible-token rocket-ship uint)
;; a map from rocket ships to their allowed
;; pilots
(define-map allowed-pilots
((rocket-ship uint)) ((pilots (list 10 principal))))
;; implementing a contains function via fold
(define-private (contains-check
(y principal)
(to-check { p: principal, result: bool }))
(if (get result to-check)
to-check
{ p: (get p to-check),
result: (is-eq (get p to-check) y) }))
(define-private (contains (x principal) (find-in (list 10 principal)))
(get result (fold contains-check find-in
{ p: x, result: false })))
(define-read-only (is-my-ship (ship uint))
(is-eq (some tx-sender) (nft-get-owner? rocket-ship ship)))
;; this function will print a message
;; (and emit an event) if the tx-sender was
;; an authorized flyer.
;;
;; here we use tx-sender, because we want
;; to allow the user to let other contracts
;; fly the ship on behalf of users
(define-public (fly-ship (ship uint))
(let ((pilots (default-to
(list)
(get pilots (map-get? allowed-pilots { rocket-ship: ship })))))
(if (contains tx-sender pilots)
(begin (print "Flew the rocket-ship!")
(ok true))
(begin (print "Tried to fly without permission!")
(ok false)))))
;;
;; Authorize a new pilot.
;;
;; here we want to ensure that this function
;; was called _directly_ by the user by
;; checking that tx-sender and contract-caller are equal.
;; if any other contract is in the call stack, contract-caller
;; would be updated to a different principal.
;;
(define-public (authorize-pilot (ship uint) (pilot principal))
(begin
;; sender must equal caller: an intermediate contract is
;; not issuing this call.
(asserts! (is-eq tx-sender contract-caller) (err u1))
;; sender must own the rocket ship
(asserts! (is-eq (some tx-sender)
(nft-get-owner? rocket-ship ship)) (err u2))
(let ((prev-pilots (default-to
(list)
(get pilots (map-get? allowed-pilots { rocket-ship: ship })))))
;; don't add a pilot already in the list
(asserts! (not (contains pilot prev-pilots)) (err u3))
;; append to the list, and check that it is less than
;; the allowed maximum
(match (as-max-len? (append prev-pilots pilot) u10)
next-pilots
(ok (map-set allowed-pilots {rocket-ship: ship} {pilots: next-pilots}))
;; too many pilots already
(err u4)))))
扩展功能:多人飞船智能合约
fly-ship 的授权模式允许飞行员从其他合约程序中驾驶火箭飞船。这允许其他合约程序可以围绕调用该函数周边,构建的新功能。
例如,我们可以创建一个合约程序,在单个交易中为多个火箭飞船调用 fly-ship 函数:
;;
;; rockets-multi.clar
;;
(define-private (call-fly (ship uint))
(unwrap! (contract-call? .rockets-base fly-ship ship) false))
;; try to fly all the ships, returning a list of whether
;; or not we were able to fly the supplied ships
(define-public (fly-all (ships (list 10 uint)))
(ok (map call-fly ships)))
给合约程序拥有的资产授权
核对 authorize-pilot 函数,将保护用户免受恶意合约程序的侵害,但这样的方案如何支持合约程序所拥有的资产呢?这就是 as-contract 函数的用途。as-contract 函数执行提供的闭包,如果交易的发送者就是当前合约程序,而不是用户——它通过将 tx-sender 更新为当前合约程序的实体用户来实现。例如,我们可以使用它来创建智能合约 rocket-ship-line:
;;
;; rockets-ship-line.clar
;;
(define-constant line-ceo 'SP19Y6GRV9X778VFS1V8WT2H502WFK33XZXADJWZ)
(define-data-var employed-pilots (list 10 principal) (list))
;; This function will:
;; * check that it is called by the line-ceo
;; * check that the rocket is owned by the contract
;; * authorize each employed pilot to the ship
(define-public (add-managed-rocket (ship uint))
(begin
;; only the ceo may call this function
(asserts! (is-eq tx-sender contract-caller line-ceo) (err u1))
;; start executing as the contract
(as-contract (begin
;; make sure the contract owns the ship
(asserts! (contract-call? .rockets-base is-my-ship ship) (err u2))
;; register all of our pilots on the ship
(add-pilots-to ship)))))
;; add all the pilots to a ship using fold --
;; the fold checks the return type of previous calls,
;; skipping subsequent contract-calls if one fails.
(define-private (add-pilot-via-fold (pilot principal) (prior-result (response uint uint)))
(let ((ship (try! prior-result)))
(try! (contract-call? .rockets-base authorize-pilot ship pilot))
(ok ship)))
(define-private (add-pilots-to (ship uint))
(fold add-pilot-via-fold (var-get employed-pilots) (ok ship)))
为了使合约程序将每个飞行员添加到飞船上,合约程序必须调用 authorize-pilot 函数。但是,如果,合约程序希望代表合约程序拥有的飞船,而不是交易发送方执行此操作。在这种情况下,合约程序使用 as-contract 函数。
该翻译为人工翻译,转载请注明出处。原始的英文教程链接: https://docs.stacks.co/write-smart-contracts/principals#extending-functionality-multi-flyer-contract