Transaction
8BBEAA47F952BB…271C1D029EC1
Block 77,251 · index 0 · indexed
Summary
- Hash
- 8BBEAA47F952BB32A44D60DB1556FF5E2E17398D4B8372FF3C7D271C1D029EC1
- Block
- 77,251
- Size
- 29381 bytes
- Gas used
- 34,924,736 / 41,909,648
- Fee
- 41910ugnot
- Status
- success
Messages
- Package
- gno.land/r/gnoswap/gnft
Arguments · 19
- #1gnft
- #2README.md
- #3# GNFT GRC721-compatible NFT contract for GnoSwap LP positions. ## Overview GNFT represents each liquidity position as a unique NFT. It exposes ownership, transfer, approval, and metadata operations while generating compact SVG artwork for tokens whose URI is stored in GNFT's parameter format. GNFT implements the position-NFT surface used by GnoSwap; it does not expose every optional GRC721 extension. In particular, this package has no token enumeration API and does not expose `safeTransferFrom`. ## Core Features ### Ownership and approvals - Transfer, single-token approval, and operator approval - Owner, balance, existence, and approval queries - Staked tokens are locked to the staker contract - Other transfers use the GRC721 owner and approval checks ### Dynamic SVG generation - Generated tokens store compact gradient parameters - Parameters are rendered to SVG and returned as a base64-encoded data URI - Rendering occurs on `TokenURI` reads rather than storing the full SVG - A custom non-empty URI set with `SetTokenURI` is returned unchanged when it is not in GNFT parameter format ### Storage - Compact parameter storage reduces per-token metadata size - Template-based SVG generation avoids storing repeated markup ## Key Functions ### `Mint` Mints a new NFT for an LP position. Only the position role may call it. **Parameters:** - `cur realm`: Current realm context - `to address`: Recipient address - `tid grc721.TokenID`: Token ID to mint **Returns:** `grc721.TokenID` ### `Burn` Burns an NFT when its position is closed. Only the position role may call it. **Parameters:** - `cur realm`: Current realm context - `tid grc721.TokenID`: Token ID to burn ### `TransferFrom` Transfers NFT ownership. **Parameters:** - `cur realm`: Current realm context - `from address`: Current owner - `to address`: New owner - `tid grc721.TokenID`: Token ID to transfer For a token held by the staker contract, only the staker can move it. For other tokens, the owner, token approval, or operator approval must authorize the transfer. ### `TokenURI` Returns metadata for a token. When the stored URI parses as GNFT image parameters (`x1,y1,x2,y2,color1,color2`), GNFT renders those parameters as an SVG and returns a base64-encoded data URI. A custom non-empty URI that is not in that parameter format is returned unchanged. **Parameters:** - `tid grc721.TokenID`: Token ID **Returns:** Token URI string and an error when the token or metadata is missing ### `SetTokenURI` Sets a non-empty token URI. Only the position role may call it. **Parameters:** - `cur realm`: Current realm context - `tid grc721.TokenID`: Token ID - `tURI string`: Non-empty metadata URI ### `Approve` Approves an address to manage a specific token. **Parameters:** - `cur realm`: Current realm context - `approved address`: Address to approve - `tid grc721.TokenID`: Token ID ### `SetApprovalForAll` Approves or revokes an operator for all tokens owned by the caller. **Parameters:** - `cur realm`: Current realm context - `operator address`: Operator address - `approved bool`: Approval status ### Queries - `Name() string`: Collection name - `Symbol() string`: Collection symbol - `TotalSupply() int64`: Number of minted NFTs - `BalanceOf(owner address) (int64, error)`: Number of NFTs owned - `OwnerOf(tid grc721.TokenID) (address, error)`: Current owner - `MustOwnerOf(tid grc721.TokenID) address`: Owner or panic on error - `Exists(tid grc721.TokenID) bool`: Whether a token exists - `GetApproved(tid grc721.TokenID) (address, error)`: Token approval - `IsApprovedForAll(owner, operator address) bool`: Operator approval ## SVG Generation ### Parameter Format Generated token URIs store compact parameters: ``` "x1,y1,x2,y2,#COLOR1,#COLOR2" Example: "10,12,125,123,#FF5733,#33B5FF" ``` ### Parameter Ranges - `x1`: 7-13 - `y1`: 7-13 - `x2`: 121-126 - `y2`: 121-126 - colors: 6-digit hex (`#RRGGBB`) ### Rendering Process 1. **Mint**: Generate pseudo-random parameters and store them as a CSV string 2. **TokenURI**: Parse the CSV, generate SVG, encode it as base64, and return a data URI 3. **Display**: The browser decodes the data URI and renders the SVG The parameters use time-seeded `math/rand`; this is pseudo-random artwork generation, not a security or cryptographic randomness source. ## Usage These helpers illustrate calls from an integrating realm. `mintExample` and `burnExample` require that realm to hold the position role; `transferExample` requires the caller to satisfy the ownership/approval rules described above. ```go import ( "gno.land/p/nt/grc721/v0" "gno.land/r/gnoswap/gnft" ) func mintExample(cur realm, owner address, tokenID grc721.TokenID) grc721.TokenID { return gnft.Mint(cross(cur), owner, tokenID) } func metadataExample(tokenID grc721.TokenID) (string, error) { return gnft.TokenURI(tokenID) } func transferExample(cur realm, from, to address, tokenID grc721.TokenID) error { return gnft.TransferFrom(cross(cur), from, to, tokenID) } func burnExample(cur realm, tokenID grc721.TokenID) { gnft.Burn(cross(cur), tokenID) } ``` ## Security and Access Control - `Mint`, `SetTokenURI`, and `Burn` require the position role - Staker-held tokens can only be moved by the staker contract - Other transfers are checked by the owner/approval rules in the GRC721 ledger - Token URI parameters are validated before generated artwork is rendered - Generated artwork uses pseudo-random, time-seeded parameters and must not be treated as a source of secure randomness ## Architecture ### Dependencies - `gno.land/p/nt/grc721/v0`: GRC721 token and ledger implementation - `gno.land/p/nt/grc721/metadata/v0`: Metadata storage - `gno.land/r/gnoswap/rbac/v1`: Access control - `gno.land/r/gnoswap/access/v1`: Position-role authorization and role mirror ### State Variables - `token`: GRC721 token metadata and supply state - `ledger`: Ownership, transfer, and approval ledger - `meta`: Token URI metadata - `metaLedger`: Metadata update ledger Generated image tokens store compact parameters in metadata; the full SVG data URI is generated when `TokenURI` is called.
- #4assert.gno
- #5package gnft import ( prabc "gno.land/p/gnoswap/rbac/v1" "gno.land/p/nt/grc721/v0" "gno.land/p/nt/ufmt/v0" "gno.land/r/gnoswap/access/v1" ) // assertIsValidTokenURI panics if the token already has a URI set. func assertIsValidTokenURI(tid grc721.TokenID) { uri, _ := meta.TokenURI(tid) if uri != "" { panic(makeErrorWithDetails(errCannotSetURI, ufmt.Sprintf("token id (%s) has already set URI", string(tid)))) } } // assertIsNonEmptyTokenURI panics if the new URI is empty. func assertIsNonEmptyTokenURI(tURI string) { if tURI == "" { panic(makeErrorWithDetails(errCannotSetURI, "empty token URI")) } } // assertIsValidAddress panics if the address is invalid. func assertIsValidAddress(addr address) { if !addr.IsValid() { panic(makeErrorWithDetails(errInvalidAddress, ufmt.Sprintf("address (%s)", addr.String()))) } } // assertFromIsValidAddress panics if the from address is invalid. func assertFromIsValidAddress(from address) { if !from.IsValid() { panic(makeErrorWithDetails(errInvalidAddress, ufmt.Sprintf("from address (%s)", from.String()))) } } // assertToIsValidAddress panics if the to address is invalid. func assertToIsValidAddress(to address) { if !to.IsValid() { panic(makeErrorWithDetails(errInvalidAddress, ufmt.Sprintf("to address (%s)", to.String()))) } } // assertIsAllowedTransfer enforces that NFTs held // by the staker (i.e. currently staked) can only be moved by the staker itself. // Other tokens fall through to the standard GRC721 owner/approval checks performed inside ledger. func assertIsAllowedTransfer(caller address, tid grc721.TokenID) { owner, err := token.OwnerOf(tid) if err != nil { // Surface the same not-found error as the standard transfer path. checkErr(err) } stakerAddr := access.MustGetAddress(prabc.ROLE_STAKER.String()) if owner != stakerAddr { return } if caller != stakerAddr { panic(makeErrorWithDetails( errStakedTokenLocked, ufmt.Sprintf("token (%s) is held by staker", string(tid)), )) } }
- #6doc.gno
- #7// Package gnft implements the position-NFT surface used by GnoSwap. // // It provides GRC721-compatible ownership, transfer, approval, and metadata // operations for LP positions. Generated parameter-format URIs become SVG data // URIs on read; custom non-empty URIs are returned unchanged. Enumeration and // safeTransferFrom are not exposed by this package. package gnft
- #8errors.gno
- #9package gnft import ( ufmt "gno.land/p/nt/ufmt/v0" ) const ( errCannotSetURI = "[GNOSWAP-GNFT-001] cannot set URI" errInvalidAddress = "[GNOSWAP-GNFT-002] invalid address" // Transfer errors errNotOwnerOrApproved = "[GNOSWAP-GNFT-003] caller is not token owner or approved" errTokenNotExists = "[GNOSWAP-GNFT-004] token does not exist" errTransferToSelf = "[GNOSWAP-GNFT-005] cannot transfer to self" errInvalidTokenParams = "[GNOSWAP-GNFT-006] invalid token parameters" errInvalidTokenParamsRange = "[GNOSWAP-GNFT-007] token parameters out of range" errInvalidColorFormat = "[GNOSWAP-GNFT-008] invalid color format" errStakedTokenLocked = "[GNOSWAP-GNFT-009] staked token is locked" errSpoofedRealm = "[GNOSWAP-GNFT-010] rlm does not match the current crossing frame" ) // makeErrorWithDetails creates an error with additional context. func makeErrorWithDetails(message string, details string) error { return ufmt.Errorf("%s || %s", message, details) }
- #10gnft.gno
- #11package gnft import ( "chain" "errors" "gno.land/p/nt/grc721/metadata/v0" "gno.land/p/nt/grc721/v0" "gno.land/p/nt/seqid/v0" "gno.land/p/nt/ufmt/v0" "gno.land/r/gnoswap/access/v1" prabc "gno.land/p/gnoswap/rbac/v1" _ "gno.land/r/gnoswap/rbac/v1" ) var ( token *grc721.Token ledger *grc721.PrivateLedger meta *metadata.Metadata metaLedger *metadata.Ledger ) func init(cur realm) { token, ledger = grc721.NewToken("GNOSWAP NFT", "GNFT", seqid.ID(0), cur) meta, metaLedger = metadata.NewMetadata(ledger) } // Name returns the NFT collection name. // // Returns: // - name: collection name func Name() string { return token.GetName() } // Symbol returns the NFT collection symbol. // // Returns: // - symbol: collection symbol func Symbol() string { return token.GetSymbol() } // TotalSupply returns the total number of NFTs minted. // // Returns: // - supply: number of NFTs minted by the collection func TotalSupply() int64 { return token.TotalSupply() } // TokenURI returns the metadata URI for the specified token ID. // Parameter-format values (x1,y1,x2,y2,color1,color2) are rendered as a // base64-encoded SVG data URI; other non-empty stored URIs are returned unchanged. // // Parameters: // - tid: token ID whose metadata URI is requested // // Returns: // - uri: stored URI, or a generated SVG data URI for parameter-format metadata // - error: metadata lookup error when tid has no retrievable metadata func TokenURI(tid grc721.TokenID) (string, error) { stored, err := meta.TokenURI(tid) if err != nil { return "", err } params, err := parseImageParams(stored) if err == nil { return params.generateImageURI(tid), nil } return stored, nil } // BalanceOf returns the number of NFTs owned by the specified address. // // Parameters: // - owner: address whose NFT balance is queried // // Returns: // - balance: number of NFTs owned by owner // - error: token-ledger balance lookup error; an invalid owner address // panics before the lookup func BalanceOf(owner address) (int64, error) { assertIsValidAddress(owner) return token.BalanceOf(owner) } // OwnerOf returns the owner address for the specified token ID. // // Parameters: // - tid: token ID whose owner is requested // // Returns: // - owner: current owner address // - error: token-ledger error when tid does not exist or cannot be read func OwnerOf(tid grc721.TokenID) (address, error) { return token.OwnerOf(tid) } // MustOwnerOf returns the owner address for the specified token ID. // It panics if the token ID is invalid. // // Parameters: // - tid: token ID whose owner is requested // // Returns: // - owner: current owner address; panics when tid is invalid or absent func MustOwnerOf(tid grc721.TokenID) address { ownerAddr, err := token.OwnerOf(tid) checkErr(err) return ownerAddr } // SetTokenURI sets the metadata URI for the specified token. // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // - tid: token ID whose metadata URI is replaced // - tURI: non-empty metadata URI or parameter-format image description // // Returns: // - updated: true after the URI is stored and re-read successfully // - error: nil on success; validation and storage failures panic before a // non-nil error can be returned // // Only callable by position contract. func SetTokenURI(cur realm, tid grc721.TokenID, tURI string) (bool, error) { caller := cur.Previous().Address() access.AssertIsPosition(caller) assertIsNonEmptyTokenURI(tURI) assertIsValidTokenURI(tid) checkErr(setTokenURI(0, cur, tid, tURI)) return true, nil } // TransferFrom transfers a token from one address to another. // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // - from: current owner address // - to: recipient address // - tid: token ID to transfer // // Returns: // - error: nil after a successful transfer; invalid addresses, authorization, // ownership, or ledger failures panic through checkTransferErr // // Permission model: // - Tokens held by the staker contract (i.e. currently staked) can only be // moved by the staker itself; the underlying staked LP position is // non-transferable. // - Otherwise, ownership and approval are enforced by the GRC721 layer // (owner / approved-for-token / approved-for-all). func TransferFrom(cur realm, from, to address, tid grc721.TokenID) error { assertFromIsValidAddress(from) assertToIsValidAddress(to) caller := cur.Previous().Address() assertIsAllowedTransfer(caller, tid) err := ledger.TransferFrom(caller, from, to, tid) checkTransferErr(err, caller, from, to, tid) return nil } // Approve grants permission to transfer a specific token ID to another address. // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // - approved: address to approve // - tid: token ID to approve for transfer // // Returns: // - error: nil after approval is stored; invalid addresses, ownership, or // ledger failures panic through checkApproveErr func Approve(cur realm, approved address, tid grc721.TokenID) error { assertIsValidAddress(approved) caller := cur.Previous().Address() err := ledger.Approve(caller, approved, tid) checkApproveErr(err, caller, approved, tid) return nil } // SetApprovalForAll enables/disables operator approval for all tokens. // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // - operator: address to set approval for // - approved: true to approve, false to revoke // // Returns: // - error: nil after the operator approval is stored; ledger failures panic // before a non-nil error can be returned func SetApprovalForAll(cur realm, operator address, approved bool) error { assertIsValidAddress(operator) checkErr(ledger.SetApprovalForAll(cur.Previous().Address(), operator, approved)) return nil } // GetApproved returns approved address for token ID. // // Parameters: // - tid: token ID whose per-token approval is requested // // Returns: // - approved: address approved for tid // - error: token-ledger error if tid does not exist or approval cannot be read func GetApproved(tid grc721.TokenID) (address, error) { return token.GetApproved(tid) } // IsApprovedForAll checks if operator can manage all owner's tokens. // // Parameters: // - owner: token owner address // - operator: operator address to check // // Returns: // - approved: true when operator is approved for every token owned by owner func IsApprovedForAll(owner, operator address) bool { return token.IsApprovedForAll(owner, operator) } // Mint creates new NFT and transfers it to to. // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // - to: recipient address // - tid: token ID to mint // // Returns: // - tokenID: minted token ID // // Only callable by position contract. func Mint(cur realm, to address, tid grc721.TokenID) grc721.TokenID { caller := cur.Previous().Address() access.AssertIsPosition(caller) positionAddr := access.MustGetAddress(prabc.ROLE_POSITION.String()) checkErr(ledger.Mint(positionAddr, tid)) // Store only the gradient parameters instead of full base64 SVG to reduce storage costs. // Parameters are converted to full SVG on read via TokenURI(). imageParams := genImageParamsString(generateRandInstance()) checkErr(setTokenURI(0, cur, tid, imageParams)) checkErr(ledger.TransferFrom(positionAddr, positionAddr, to, tid)) return tid } // Exists checks if token ID exists. // // Parameters: // - tid: token ID to check // // Returns: // - exists: true when the ledger can resolve an owner for tid; false otherwise func Exists(tid grc721.TokenID) bool { _, err := token.OwnerOf(tid) return err == nil } // Burn removes a specific token ID. // // Parameters: // - cur: Current realm context; callers use cross(cur) when crossing into this realm. // - tid: token ID to burn // // Only callable by position. func Burn(cur realm, tid grc721.TokenID) { caller := cur.Previous().Address() access.AssertIsPosition(caller) checkErr(ledger.Burn(tid)) } // Render returns the HTML representation of the NFT. // // Parameters: // - path: render path; the empty path selects the collection home page // // Returns: // - html: collection HTML for the home path, or "404\n" for unsupported paths func Render(path string) string { if path == "" { return token.RenderHome() } return "404\n" } // setTokenURI sets the metadata URI for a specific token ID. func setTokenURI(_ int, rlm realm, tid grc721.TokenID, tURI string) error { if !rlm.IsCurrent() { return errors.New(errSpoofedRealm) } previousRealm := rlm.Previous() previousAddr := previousRealm.Address() err := metaLedger.SetTokenURI(tid, tURI) if err != nil { return makeErrorWithDetails(err.Error(), ufmt.Sprintf("token id (%s)", tid)) } tokenURI, err := TokenURI(tid) if err != nil { return makeErrorWithDetails(err.Error(), ufmt.Sprintf("token id (%s)", tid)) } chain.Emit( "SetTokenURI", "prevAddr", previousAddr.String(), "prevRealm", previousRealm.PkgPath(), "tokenId", string(tid), "tokenURI", tokenURI, ) return nil }
- #12gnomod.toml
- #13module = "gno.land/r/gnoswap/gnft" gno = "0.9"
- #14svg_generator.gno
- #15package gnft import ( b64 "encoding/base64" "errors" "math/rand" "strconv" "strings" "gno.land/p/nt/grc721/v0" ufmt "gno.land/p/nt/ufmt/v0" ) // MUST BE IMMUTABLE, DO NOT MODIFY. // SVG template structure: // The template is split at variable insertion points for efficient string concatenation. // Full template with placeholders (for reference): // // <svg width="135" height="135" viewBox="0 0 135 135" fill="none" xmlns="..."> // <g clip-path="url(#clip0_7698_56846)"> // <circle cx="67.5" cy="67.5" r="67.5" fill="url(#paint0_linear_7698_56846)"/> // ... (path elements) ... // </g> // <defs> // <linearGradient id="paint0_linear_7698_56846" // x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" <-- variables // gradientUnits="userSpaceOnUse"> // <stop stop-color="{color1}"/> <-- variable // <stop offset="1" stop-color="{color2}"/> <-- variable // </linearGradient> // ... // </defs> // </svg> // svgTemplate holds pre-split template parts for efficient concatenation. // Usage: svgTemplate[0] + clipID + svgTemplate[1] + paintID + ... + color2 + svgTemplate[10] var svgTemplate = [11]string{ // [0] SVG header and body (before clip-path id) `<svg width="135" height="135" viewBox="0 0 135 135" fill="none" xmlns="http://www.w3.org/2000/svg"> <g clip-path="url(#`, // [1] between clip-path id and fill id `)"> <circle cx="67.5" cy="67.5" r="67.5" fill="url(#`, // [2] between fill id and gradient id `)"/> <path d="M51.2905 42.9449L66.4895 33L97 52.8061L81.8241 62.7425L51.2905 42.9449Z" fill="white"/> <path d="M51.6055 67.5059L66.8044 57.561L97 77.0657L82.1046 87.1793L51.6055 67.5059Z" fill="white" fill-opacity="0.4"/> <path d="M36.0464 81.7559L51.2905 71.811L81.7336 91.6547L66.4895 101.508L36.0464 81.7559Z" fill="white" fill-opacity="0.6"/> <path d="M36.001 52.8055L51.2884 42.9177L51.2884 71.8145L36.001 81.779L36.001 52.8055Z" fill="white"/> <path d="M82.1051 87.1797L97.0016 77.0662L97.0016 81.7029L81.7896 91.629L82.1051 87.1797Z" fill="white" fill-opacity="0.5"/> </g> <defs> <linearGradient id="`, // [3] between gradient id and x1 `" x1="`, // [4] between x1 and y1 `" y1="`, // [5] between y1 and x2 `" x2="`, // [6] between x2 and y2 `" y2="`, // [7] between y2 and color1 `" gradientUnits="userSpaceOnUse"> <stop stop-color="`, // [8] between color1 and color2 `"/> <stop offset="1" stop-color="`, // [9] between color2 and clipPath id `"/> </linearGradient> <clipPath id="`, // [10] SVG footer (after clipPath id) `"> <rect width="135" height="135" fill="white"/> </clipPath> </defs> </svg> `, } // charset contains valid hex digits for color generation. const charset = "0123456789ABCDEF" // Parameter range constants for gradient coordinates. const ( x1Min = 7 x1Max = 13 y1Min = 7 y1Max = 13 x2Min = 121 x2Max = 126 y2Min = 121 y2Max = 126 x1Range = x1Max - x1Min + 1 y1Range = y1Max - y1Min + 1 x2Range = x2Max - x2Min + 1 y2Range = y2Max - y2Min + 1 ) // genImageParamsString generates random gradient parameters and returns them as a compact string. // Format: "x1,y1,x2,y2,color1,color2" (e.g., "10,12,125,123,#AABBCC,#DDEEFF") func genImageParamsString(r *rand.Rand) string { x1 := x1Min + r.Uint64N(x1Range) y1 := y1Min + r.Uint64N(y1Range) x2 := x2Min + r.Uint64N(x2Range) y2 := y2Min + r.Uint64N(y2Range) var buf1 [7]byte var buf2 [7]byte buf1[0] = '#' buf2[0] = '#' for i := 1; i < 7; i++ { buf1[i] = charset[r.IntN(16)] buf2[i] = charset[r.IntN(16)] } color1 := string(buf1[:]) color2 := string(buf2[:]) return strconv.Itoa(int(x1)) + "," + strconv.Itoa(int(y1)) + "," + strconv.Itoa(int(x2)) + "," + strconv.Itoa(int(y2)) + "," + color1 + "," + color2 } // ImageParams holds parsed and validated image parameters. type ImageParams struct { x1, y1, x2, y2 int color1, color2 string } // validateCoordinate checks if a coordinate value is within valid range. // Returns error with details about which coordinate failed if out of range. func validateCoordinate(value int, min int, max int, name string) error { if value < min || value > max { details := ufmt.Sprintf("%s=%d (expected range: [%d, %d])", name, value, min, max) return makeErrorWithDetails(errInvalidTokenParamsRange, details) } return nil } // parseImageParams parses and validates parameters in one step. // Returns parsed ImageParams pointer or error. // Returns nil on error to avoid allocating zero-value struct. // Expected format: "x1,y1,x2,y2,color1,color2" func parseImageParams(s string) (*ImageParams, error) { parts := strings.Split(s, ",") if len(parts) != 6 { return nil, errors.New(errInvalidTokenParams) } x1, err := strconv.Atoi(parts[0]) if err != nil { return nil, errors.New(errInvalidTokenParams) } y1, err := strconv.Atoi(parts[1]) if err != nil { return nil, errors.New(errInvalidTokenParams) } x2, err := strconv.Atoi(parts[2]) if err != nil { return nil, errors.New(errInvalidTokenParams) } y2, err := strconv.Atoi(parts[3]) if err != nil { return nil, errors.New(errInvalidTokenParams) } // Validate coordinate ranges with detailed error messages if err := validateCoordinate(x1, x1Min, x1Max, "x1"); err != nil { return nil, err } if err := validateCoordinate(y1, y1Min, y1Max, "y1"); err != nil { return nil, err } if err := validateCoordinate(x2, x2Min, x2Max, "x2"); err != nil { return nil, err } if err := validateCoordinate(y2, y2Min, y2Max, "y2"); err != nil { return nil, err } color1 := parts[4] color2 := parts[5] // Validate color format (#XXXXXX) if !isValidHexColor(color1) || !isValidHexColor(color2) { return nil, errors.New(errInvalidColorFormat) } return &ImageParams{ x1: x1, y1: y1, x2: x2, y2: y2, color1: color1, color2: color2, }, nil } // generateImageURI converts parsed image parameters to a base64-encoded SVG image URI. func (params ImageParams) generateImageURI(tid grc721.TokenID) string { svg := params.generateSVG(tid) sEnc := b64.StdEncoding.EncodeToString([]byte(svg)) return "data:image/svg+xml;base64," + sEnc } // generateSVG generates SVG image from parsed and validated parameters. func (params ImageParams) generateSVG(tid grc721.TokenID) string { suffix := svgIDSuffix(tid) paintID := "paint0_linear_" + suffix clipID := "clip0_" + suffix return svgTemplate[0] + clipID + svgTemplate[1] + paintID + svgTemplate[2] + paintID + svgTemplate[3] + strconv.Itoa(params.x1) + svgTemplate[4] + strconv.Itoa(params.y1) + svgTemplate[5] + strconv.Itoa(params.x2) + svgTemplate[6] + strconv.Itoa(params.y2) + svgTemplate[7] + params.color1 + svgTemplate[8] + params.color2 + svgTemplate[9] + clipID + svgTemplate[10] } func svgIDSuffix(tid grc721.TokenID) string { return "gnft_" + string(tid) } // isValidHexColor checks if a string is a valid hex color in #XXXXXX format. func isValidHexColor(color string) bool { if len(color) != 7 || color[0] != '#' { return false } for i := 1; i < 7; i++ { c := color[i] isHex := (c >= '0' && c <= '9') || (c >= 'A' && c <= 'F') || (c >= 'a' && c <= 'f') if !isHex { return false } } return true }
- #16utils.gno
- #17package gnft import ( "errors" "math/rand" "time" "gno.land/p/nt/grc721/v0" "gno.land/p/nt/ufmt/v0" ) // generateRandInstance generates a new random instance. func generateRandInstance() *rand.Rand { now := time.Now() seed1 := now.Unix() + TotalSupply() seed2 := now.UnixNano() + TotalSupply() pcg := rand.NewPCG(uint64(seed1), uint64(seed2)) return rand.New(pcg) } // checkErr panics if an error occurs. func checkErr(err error) { if err != nil { panic(err.Error()) } } // checkTransferErr wraps transfer errors with more specific context. func checkTransferErr(err error, caller, from, to address, tid grc721.TokenID) { if err == nil { return } // Check if token exists owner, ownerErr := token.OwnerOf(tid) if ownerErr != nil { panic(ownerErr) } switch err { case grc721.ErrCallerIsNotOwnerOrApproved: // Check if caller is the owner if caller == owner { panic(makeErrorWithDetails(grc721.ErrTransferFromIncorrectOwner.Error(), ufmt.Sprintf("owner mismatch - from: %s, actual owner: %s, token: %s", from, owner, string(tid)))) } // Check if caller is approved for this specific token approved, _ := token.GetApproved(tid) if approved != caller { // Check if caller is approved for all tokens if !token.IsApprovedForAll(owner, caller) { panic(makeErrorWithDetails(grc721.ErrCallerIsNotOwnerOrApproved.Error(), ufmt.Sprintf("caller %s is not owner %s or approved for token %s", caller, owner, string(tid)))) } } case grc721.ErrInvalidAddress: panic(makeErrorWithDetails(grc721.ErrInvalidAddress.Error(), ufmt.Sprintf("to address (%s)", to))) case grc721.ErrTransferFromIncorrectOwner: panic(makeErrorWithDetails(grc721.ErrTransferFromIncorrectOwner.Error(), ufmt.Sprintf("from %s is not the owner %s of token %s", from, owner, string(tid)))) case grc721.ErrInvalidTokenId: panic(makeErrorWithDetails(grc721.ErrInvalidTokenId.Error(), ufmt.Sprintf("token %s", string(tid)))) default: panic(err.Error()) } } // checkApproveErr wraps approve errors with more specific context. func checkApproveErr(err error, caller, approved address, tid grc721.TokenID) { if err == nil { return } switch { case errors.Is(err, grc721.ErrInvalidTokenId): panic(makeErrorWithDetails(errTokenNotExists, ufmt.Sprintf("token %s", string(tid)))) case errors.Is(err, grc721.ErrCallerIsNotOwnerOrApproved): owner, ownerErr := token.OwnerOf(tid) checkErr(ownerErr) panic(makeErrorWithDetails(errNotOwnerOrApproved, ufmt.Sprintf("caller %s cannot approve for token %s owned by %s", caller, string(tid), owner))) case errors.Is(err, grc721.ErrApprovalToCurrentOwner): panic(makeErrorWithDetails(errTransferToSelf, ufmt.Sprintf("cannot approve to current owner %s for token %s", approved, string(tid)))) default: panic(err.Error()) } }
- #18/gno.MemPackageType
- #19 MPUserAll
Result log
msg:0,success:true,log:,events:[]