$npx -y skills add lightninglabs/lightning-agent-tools --skill taproot-assets-rpcWorking guide for LND + Taproot Assets gRPC in Go — authentication, asset amounts, channel data, pagination, and common gotchas discovered building a real wallet.
| 1 | Use this guide when writing Go code that calls LND (`lnrpc`) or Taproot Assets (`taprpc`, `tapchannelrpc`) via gRPC through a `litd` instance. |
| 2 | |
| 3 | --- |
| 4 | |
| 5 | # LND + Taproot Assets gRPC — Working Guide |
| 6 | |
| 7 | ## Authentication |
| 8 | |
| 9 | Taproot Assets lives behind `litd`, which wraps `lnd` and `tapd` behind a single gRPC endpoint. Authentication requires two steps: |
| 10 | |
| 11 | 1. Connect with `lit.macaroon` and call `BakeSuperMacaroon`. |
| 12 | 2. Close that connection and reconnect with the supermacaroon — it grants access to all subservers. |
| 13 | |
| 14 | ```go |
| 15 | // Step 1: bake supermacaroon |
| 16 | litMac, _ := loadMacaroon(cfg.MacaroonPath) // binary macaroon file |
| 17 | tlsCreds, _ := credentials.NewClientTLSFromFile(cfg.TLSCertPath, "") |
| 18 | macCred, _ := newMacaroonCredential(litMac) |
| 19 | |
| 20 | conn, _ := grpc.Dial(cfg.RPCServer, |
| 21 | grpc.WithTransportCredentials(tlsCreds), |
| 22 | grpc.WithPerRPCCredentials(macCred), |
| 23 | ) |
| 24 | proxy := litrpc.NewProxyClient(conn) |
| 25 | resp, _ := proxy.BakeSuperMacaroon(ctx, &litrpc.BakeSuperMacaroonRequest{RootKeyIdSuffix: 0}) |
| 26 | conn.Close() |
| 27 | |
| 28 | // Step 2: reconnect with supermacaroon |
| 29 | superMac, _ := parseMacaroonHex(resp.Macaroon) |
| 30 | superCred, _ := newMacaroonCredential(superMac) |
| 31 | mainConn, _ := grpc.Dial(cfg.RPCServer, |
| 32 | grpc.WithTransportCredentials(tlsCreds), |
| 33 | grpc.WithPerRPCCredentials(superCred), |
| 34 | grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(200 * 1024 * 1024)), |
| 35 | ) |
| 36 | ``` |
| 37 | |
| 38 | The macaroon credential satisfies `credentials.PerRPCCredentials`: |
| 39 | |
| 40 | ```go |
| 41 | func (m *macaroonCredential) GetRequestMetadata(_ context.Context, _ ...string) (map[string]string, error) { |
| 42 | data, _ := m.mac.MarshalBinary() |
| 43 | return map[string]string{"macaroon": hex.EncodeToString(data)}, nil |
| 44 | } |
| 45 | func (m *macaroonCredential) RequireTransportSecurity() bool { return true } |
| 46 | ``` |
| 47 | |
| 48 | **Default litd data dirs:** |
| 49 | |
| 50 | | OS | Path | |
| 51 | |---|---| |
| 52 | | Linux | `~/.lit/` | |
| 53 | | macOS | `~/Library/Application Support/Lit/` | |
| 54 | | Windows | `%LOCALAPPDATA%\Lit\` | |
| 55 | |
| 56 | Default macaroon: `<litdir>/<network>/lit.macaroon`. Default TLS cert: `<litdir>/tls.cert`. Default port: `8443`. |
| 57 | |
| 58 | --- |
| 59 | |
| 60 | ## Asset Amounts and Decimal Display |
| 61 | |
| 62 | Taproot Assets stores amounts as raw integers. The `DecimalDisplay` field tells you how many decimal places to shift for human display. |
| 63 | |
| 64 | **Proto field is double-nested:** |
| 65 | ```go |
| 66 | var dd uint32 |
| 67 | if asset.DecimalDisplay != nil { |
| 68 | dd = asset.DecimalDisplay.DecimalDisplay // note: field name repeated |
| 69 | } |
| 70 | ``` |
| 71 | |
| 72 | **Display (raw → human):** |
| 73 | ```go |
| 74 | func formatAssetAmount(amount uint64, decimalDisplay uint32) string { |
| 75 | if decimalDisplay == 0 { |
| 76 | return fmt.Sprintf("%d", amount) |
| 77 | } |
| 78 | div := uint64(1) |
| 79 | for i := uint32(0); i < decimalDisplay; i++ { div *= 10 } |
| 80 | whole := amount / div |
| 81 | frac := amount % div |
| 82 | return fmt.Sprintf("%d.%0*d", whole, int(decimalDisplay), frac) |
| 83 | } |
| 84 | ``` |
| 85 | |
| 86 | **Parse (human input → raw):** |
| 87 | ```go |
| 88 | func parseScaledAmount(s string, decimalDisplay uint32) (uint64, error) { |
| 89 | // Split on '.', scale integer part, pad/truncate fractional part. |
| 90 | // multiply intPart by 10^decimalDisplay, add fracPart (zero-padded to decimalDisplay digits) |
| 91 | } |
| 92 | ``` |
| 93 | |
| 94 | BTC: treat as satoshis with `decimalDisplay = 3` to get millisatoshis (matching `lnrpc.Invoice.ValueMsat`). |
| 95 | |
| 96 | --- |
| 97 | |
| 98 | ## Group Keys vs Asset IDs |
| 99 | |
| 100 | Assets can be grouped (fungible across mints) or ungrouped (unique to a single issuance). |
| 101 | |
| 102 | | Concept | Field | Size | Use when | |
| 103 | |---|---|---|---| |
| 104 | | Asset ID | `asset.AssetGenesis.AssetId` | 32 bytes | Ungrouped asset or specific UTXO | |
| 105 | | Tweaked group key | `asset.AssetGroup.TweakedGroupKey` | 33 bytes (compressed EC) | Grouped asset; identifies the whole group | |
| 106 | |
| 107 | **Deduplication pattern** — when building a picker or aggregating balances: |
| 108 | ```go |
| 109 | dedupeKey := groupKeyHex |
| 110 | if dedupeKey == "" { |
| 111 | dedupeKey = assetIDHex |
| 112 | } |
| 113 | if seen[dedupeKey] { continue } |
| 114 | seen[dedupeKey] = true |
| 115 | ``` |
| 116 | |
| 117 | **Sending/invoicing** — prefer group key over asset ID for grouped assets: |
| 118 | ```go |
| 119 | req := &tapchannelrpc.AddInvoiceRequest{AssetAmount: scaledAmt} |
| 120 | if groupKeyHex != "" { |
| 121 | req.GroupKey, _ = hex.DecodeString(groupKeyHex) |
| 122 | } else { |
| 123 | req.AssetId, _ = hex.DecodeString(assetIDHex) |
| 124 | } |
| 125 | ``` |
| 126 | |
| 127 | --- |
| 128 | |
| 129 | ## Script Key Types (Filtering Assets) |
| 130 | |
| 131 | `ListAssets` can filter by script key type — this tells you where the asset lives: |
| 132 | |
| 133 | | Constant | Value | Meaning | |
| 134 | |---|---|---| |
| 135 | | `SCRIPT_KEY_BIP86` | 1 | Wallet asset — spendable onchain | |
| 136 | | `SCRIPT_KEY_CHANNEL` | 5 | Locked in a Lightning channel | |
| 137 | | `SCRIPT_KEY_UNKNOWN` | 0 | Unclassified | |
| 138 | |
| 139 | To get all types: |
| 140 | ```go |
| 141 | resp, _ := tap.ListAssets(ctx, &taprpc.ListAssetRequest{ |
| 142 | ScriptKeyType: &taprpc.ScriptKeyTypeQuery{ |
| 143 | Type: &taprpc.ScriptKeyTypeQuery_AllTypes{AllTypes: true}, |
| 144 | }, |
| 145 | }) |
| 146 | for _, a := range resp.GetAssets() { |
| 147 | if a.ScriptKeyType == taprpc.ScriptKeyType_SCRIPT_KEY_BIP86 { /* wallet asset */ } |
| 148 | } |
| 149 | ``` |
| 150 | |
| 151 | **Critical gotcha:** Channel assets may NOT be tagged `SCRIPT_KEY_CHANNEL` in ` |