titan-iac/scripts/crypto_wallet_sui_setup.fish

769 lines
30 KiB
Fish
Raw Normal View History

2025-08-14 20:37:30 -05:00
### --------- helpers ----------
2025-08-19 01:06:45 -05:00
function _need --description "ensure a command exists"
2025-08-14 20:37:30 -05:00
for c in $argv
if not type -q $c
echo (set_color red)"Error:"(set_color normal)" missing command: $c" >&2
return 1
end
end
end
2025-08-19 01:06:45 -05:00
function _banner --description "pretty section header" -a MSG
2025-08-14 20:37:30 -05:00
set_color cyan; echo; echo "==> $MSG"; set_color normal
end
2025-08-19 01:06:45 -05:00
# 32-char alphanumeric generator (fast, no shell-specials)
function _rand_alnum32
if type -q openssl
openssl rand -base64 48 2>/dev/null | tr -dc 'A-Za-z0-9' | head -c 32
else
dd if=/dev/urandom bs=48 count=1 2>/dev/null | base64 | tr -dc 'A-Za-z0-9' | head -c 32
end
end
# Default image chooser (you should override with your own multi-arch image)
function _sui_default_image -a NET
echo registry.bstein.dev/infra/sui-tools:1.53.2
end
# Convert any string to a k8s-safe name (RFC-1123 label-ish)
function _k8s_name --description "sanitize to RFC-1123 label" -a S
2025-08-14 20:37:30 -05:00
set s (string lower -- $S)
set s (string replace -ar -- '[^a-z0-9-]' '-' $s)
2025-08-19 01:06:45 -05:00
set s (string replace -r -- '^-+' '' $s)
set s (string replace -r -- '-+$' '' $s)
2025-08-14 20:37:30 -05:00
echo $s
end
2025-08-19 01:06:45 -05:00
# Simple non-empty check
function _require --description "require a non-empty value" -a WHAT VAL
2025-08-14 20:37:30 -05:00
if test -z "$VAL"
2025-08-19 01:06:45 -05:00
echo (set_color red)"Error:"(set_color normal)" $WHAT cannot be empty. Aborting." >&2
2025-08-14 20:37:30 -05:00
return 1
end
end
2025-08-19 01:06:45 -05:00
function _sc_list --description "list storageclasses as menu"
kubectl get storageclass -o json | jq -r '
.items[]
| {name: .metadata.name,
provisioner: .provisioner,
reclaim: (.reclaimPolicy // "Delete"),
default: ( .metadata.annotations["storageclass.kubernetes.io/is-default-class"] == "true"
or .metadata.annotations["storageclass.beta.kubernetes.io/is-default-class"] == "true") }
| "\(.name)\t\(.provisioner)\t\(.reclaim)\t\((.default|tostring))"
' 2>/dev/null
2025-08-14 20:37:30 -05:00
end
2025-08-19 01:06:45 -05:00
# Choose StorageClass by index or name
function _choose_sc --description "interactive StorageClass picker"
2025-08-14 20:37:30 -05:00
set lines (_sc_list)
if test (count $lines) -eq 0
echo (set_color red)"Error:"(set_color normal)" No StorageClasses found." >&2
return 1
end
2025-08-19 01:06:45 -05:00
2025-08-14 20:37:30 -05:00
echo "" >&2
echo "==> Available StorageClasses" >&2
echo " # | name | provisioner | reclaim | default" >&2
echo "----+----------------------+------------------------+---------+--------" >&2
set i 1
for l in $lines
set name (echo $l | awk -F'\t' '{print $1}')
set prov (echo $l | awk -F'\t' '{print $2}')
set rec (echo $l | awk -F'\t' '{print $3}')
set def (echo $l | awk -F'\t' '{print $4}')
printf " %2d | %-20s | %-22s | %-7s | %s\n" $i $name $prov $rec $def >&2
set i (math $i + 1)
end
2025-08-19 01:06:45 -05:00
2025-08-14 20:37:30 -05:00
set prefer (printf "%s\n" $lines | awk -F'\t' '$4=="true"{print $1}' | head -n1)
if test -z "$prefer"
set prefer (printf "%s\n" $lines | awk -F'\t' '{print $1}' | head -n1)
end
2025-08-19 01:06:45 -05:00
2025-08-14 20:37:30 -05:00
read -P "Pick StorageClass by number or name (blank = $prefer): " choice
2025-08-19 01:06:45 -05:00
2025-08-14 20:37:30 -05:00
if test -z "$choice"
2025-08-19 01:06:45 -05:00
echo "Using StorageClass: $prefer" >&2
2025-08-14 20:37:30 -05:00
echo $prefer
return 0
end
2025-08-19 01:06:45 -05:00
2025-08-14 20:37:30 -05:00
if string match -qr '^[0-9]+$' -- $choice
set idx (math $choice)
if test $idx -lt 1 -o $idx -gt (count $lines)
echo (set_color red)"Error:"(set_color normal)" Choice out of range." >&2
return 1
end
2025-08-19 01:06:45 -05:00
set sel (printf "%s\n" $lines[$idx] | awk -F'\t' '{print $1}')
echo "Using StorageClass: $sel" >&2
echo $sel
return 0
end
set found (printf "%s\n" $lines | awk -F'\t' -v want="$choice" '$1==want{print $1}')
if test -z "$found"
echo (set_color red)"Error:"(set_color normal)" No StorageClass named '$choice'." >&2
return 1
end
echo "Using StorageClass: $found" >&2
echo $found
end
# ---- probe Sui JSON-RPC endpoint
function _probe_sui_rpc --description "probe Sui JSON-RPC endpoint" -a URL
if test -z "$URL"
echo (set_color red)"Error:"(set_color normal)" _probe_sui_rpc needs an URL" >&2
return 1
2025-08-14 20:37:30 -05:00
end
2025-08-19 01:06:45 -05:00
set payload '{"jsonrpc":"2.0","id":1,"method":"sui_getLatestCheckpointSequenceNumber","params":[]}'
set tmp (mktemp)
set code (curl -sS -o $tmp -w "%{http_code}" -H "Content-Type: application/json" -d "$payload" "$URL")
set rc $status
set body (cat $tmp); rm -f $tmp
if test $rc -ne 0
echo (set_color red)"Transport error probing $URL (curl rc=$rc)."(set_color normal) >&2
return 1
end
if test "$code" != "200"
echo (set_color red)"HTTP $code from $URL"(set_color normal) >&2
echo $body >&2
return 1
end
set seq (printf "%s" $body | jq -r '.result // empty' 2>/dev/null)
if test -z "$seq"
echo (set_color red)"No result from RPC probe at $URL."(set_color normal) >&2
return 1
end
echo "Sui RPC OK: latest checkpoint seq=$seq"
2025-08-14 20:37:30 -05:00
end
### --------- main workflow ----------
2025-08-19 01:06:45 -05:00
function suiwallet_bootstrap --description "Interactive setup of a Sui wallet pod with PVC; creates address and prints recovery phrase"
_need kubectl jq curl awk; or return 1
2025-08-14 20:37:30 -05:00
_banner "Sui wallet bootstrap"
2025-08-19 01:06:45 -05:00
# --- Namespace
read -P "Namespace [crypto]: " ns_raw
if test -z "$ns_raw"; set ns_raw crypto; end
set ns (_k8s_name $ns_raw)
_require "Namespace" $ns; or return 1
2025-08-14 20:37:30 -05:00
echo "Ensuring namespace '$ns' exists…"
2025-08-19 01:06:45 -05:00
if kubectl get ns $ns >/dev/null 2>&1
echo "Namespace $ns already exists."
else
2025-08-14 20:37:30 -05:00
kubectl create ns $ns; or return 1
2025-08-19 01:06:45 -05:00
echo "Created namespace $ns."
2025-08-14 20:37:30 -05:00
end
2025-08-19 01:06:45 -05:00
# --- StorageClass
kubectl get storageclass
echo "TIP: Prefer a class with reclaimPolicy=Retain so a PVC delete won't delete the PV."
2025-08-14 20:37:30 -05:00
set sc (_choose_sc); or return 1
2025-08-19 01:06:45 -05:00
# --- Unified name (prefix wallet-sui-)
read -P "Wallet name (no spaces, e.g. 'brad', base will be wallet-sui-brad): " wallet_raw
2025-08-14 20:37:30 -05:00
_require "Wallet name" $wallet_raw; or return 1
2025-08-19 01:06:45 -05:00
if string match -rq '\s' -- $wallet_raw
echo (set_color red)"Error:"(set_color normal)" Wallet name cannot contain spaces." >&2
return 1
end
2025-08-14 20:37:30 -05:00
set base (_k8s_name $wallet_raw)
2025-08-19 01:06:45 -05:00
set base (string replace -r '^wallet-sui-' '' -- $base)
set name "wallet-sui-$base"
# Bind all k8s object names to the unified base
set pvc_name $name
set app_name $name
# --- Detect existing resources (idempotence)
set exists_any 0
set found_list
for r in "deploy/$app_name" (printf "netpol/%s-deny-ingress" $app_name)
if kubectl -n $ns get $r >/dev/null 2>&1
set -a found_list $r
set exists_any 1
end
end
if kubectl -n $ns get pvc $pvc_name >/dev/null 2>&1
set -a found_list "pvc/$pvc_name"
set exists_any 1
end
if kubectl -n $ns get secret (printf "%s-mnemonic" $app_name) >/dev/null 2>&1
set -a found_list (printf "secret/%s-mnemonic" $app_name)
set exists_any 1
end
if test $exists_any -eq 1
_banner "Existing resources detected for $name"
for r in $found_list
echo " - $ns/$r"
end
read -P "Remove EVERYTHING (deploy/netpol/PVC/secret) and recreate? (y/N): " wipe
if string match -qi 'y*' -- $wipe
kubectl -n $ns delete deploy $app_name --ignore-not-found --wait=true >/dev/null 2>&1; or true
kubectl -n $ns delete netpol (printf "%s-deny-ingress" $app_name) --ignore-not-found --wait=true >/dev/null 2>&1; or true
kubectl -n $ns delete pvc $pvc_name --ignore-not-found --wait=true >/dev/null 2>&1; or true
kubectl -n $ns delete secret (printf "%s-mnemonic" $app_name) --ignore-not-found >/dev/null 2>&1; or true
echo "Cleaned up previous resources."
else
echo "Reusing existing resources; they will be applied in-place."
end
end
# --- Node scheduling (worker-only by default)
read -P "Schedule only on worker nodes? (y/N) [y]: " pin_workers
if test -z "$pin_workers"; set pin_workers y; end
set use_pin 0
if string match -qi 'y*' -- $pin_workers
set use_pin 1
read -P "Worker label key [node-role.kubernetes.io/worker]: " node_label_key
if test -z "$node_label_key"; set node_label_key node-role.kubernetes.io/worker; end
read -P "Worker label value [true]: " node_label_val
if test -z "$node_label_val"; set node_label_val true; end
end
2025-08-14 20:37:30 -05:00
2025-08-19 01:06:45 -05:00
# --- Network and RPC
2025-08-14 20:37:30 -05:00
read -P "Network [mainnet|testnet|devnet] [mainnet]: " net
if test -z "$net"; set net mainnet; end
switch $net
case mainnet
set rpc_url https://fullnode.mainnet.sui.io:443
case testnet
set rpc_url https://fullnode.testnet.sui.io:443
case devnet
set rpc_url https://fullnode.devnet.sui.io:443
case '*'
echo (set_color red)"Error:"(set_color normal)" network must be mainnet|testnet|devnet"; return 1
end
2025-08-19 01:06:45 -05:00
_banner "Probing Sui RPC endpoint"
if not _probe_sui_rpc $rpc_url
echo (set_color red)"Aborting due to RPC probe failure."(set_color normal)
return 1
end
# --- Image and address alias
set image_default (_sui_default_image $net)
echo "NOTE: Prefer a multi-arch image you built (see instructions below)."
read -P "Container image [$image_default]: " image
if test -z "$image"; set image $image_default; end
2025-08-14 20:37:30 -05:00
read -P "Address alias to create [main]: " alias
if test -z "$alias"; set alias main; end
read -P "Mnemonic word length [24]: " wl
if test -z "$wl"; set wl 24; end
read -P "Store mnemonic as a Kubernetes Secret? (NOT recommended) (y/N): " store_mn
set store_mn (string lower -- $store_mn)
2025-08-19 01:06:45 -05:00
# --- Optional NetworkPolicy
read -P "Add NetworkPolicy (deny all ingress; allow egress) ? (y/N): " want_np
set want_np (string lower -- $want_np)
2025-08-14 20:37:30 -05:00
_banner "Summary"
echo " Namespace: $ns"
echo " StorageClass: $sc"
2025-08-19 01:06:45 -05:00
echo " Base name: $name"
echo " PVC: $pvc_name"
echo " App/Deployment:$app_name"
2025-08-14 20:37:30 -05:00
echo " Image: $image"
echo " Network: $net"
echo " RPC URL: $rpc_url"
echo " New alias: $alias (word length: $wl)"
2025-08-19 01:06:45 -05:00
if test $use_pin -eq 1
echo " NodeSelector: $node_label_key=$node_label_val"
end
if string match -qi 'y*' -- $want_np
echo " NetworkPolicy: deny-all ingress (egress allowed)"
end
read -P "Proceed? [y/N]: " proceed
if not string match -qi 'y*' -- $proceed
echo (set_color yellow)"Aborted by user at confirmation step."(set_color normal)
return 1
2025-08-14 20:37:30 -05:00
end
_banner "Applying PVC"
2025-08-19 01:06:45 -05:00
if kubectl -n $ns get pvc $pvc_name >/dev/null 2>&1
echo "PVC $ns/$pvc_name exists; keeping as-is."
else
set pvcfile (mktemp -t $pvc_name-pvc.XXXX.yaml)
begin
echo "apiVersion: v1"
echo "kind: PersistentVolumeClaim"
echo "metadata:"
echo " name: $pvc_name"
echo " namespace: $ns"
echo "spec:"
echo " accessModes: [\"ReadWriteOnce\"]"
echo " storageClassName: $sc"
echo " resources:"
echo " requests:"
echo " storage: 1Gi"
end > $pvcfile
echo "--- PVC manifest ($pvcfile) ---"
cat $pvcfile
kubectl apply -f $pvcfile; or return 1
2025-08-14 20:37:30 -05:00
end
_banner "Applying Deployment"
2025-08-19 01:06:45 -05:00
set dply (mktemp -t $app_name-deploy.XXXX.yaml)
begin
echo "apiVersion: apps/v1"
echo "kind: Deployment"
echo "metadata:"
echo " name: $app_name"
echo " namespace: $ns"
echo " labels: { app: $app_name }"
echo "spec:"
echo " replicas: 1"
echo " strategy: { type: Recreate }"
echo " selector: { matchLabels: { app: $app_name } }"
echo " template:"
echo " metadata:"
echo " labels: { app: $app_name }"
echo " spec:"
if test $use_pin -eq 1
echo " nodeSelector:"
echo " $node_label_key: \"$node_label_val\""
end
echo " securityContext:"
echo " fsGroup: 1000"
echo " fsGroupChangePolicy: OnRootMismatch"
echo " initContainers:"
echo " - name: volume-permissions"
echo " image: busybox:1.36"
echo " command: [\"/bin/sh\",\"-lc\",\"mkdir -p /home/sui/.sui/sui_config && chown -R 1000:1000 /home/sui/.sui && chmod -R 0770 /home/sui/.sui\"]"
echo " securityContext: { runAsUser: 0 }"
echo " volumeMounts: [ { name: data, mountPath: /home/sui/.sui } ]"
echo " containers:"
echo " - name: sui-tools"
echo " image: $image"
echo " imagePullPolicy: IfNotPresent"
echo " command: [\"/bin/sh\",\"-lc\"]"
echo " args:"
echo " - |"
echo " # keep container ready for execs"
echo " sleep infinity"
echo " volumeMounts:"
echo " - name: data"
echo " mountPath: /home/sui/.sui"
echo " resources:"
echo " requests: { cpu: \"50m\", memory: \"128Mi\" }"
echo " limits: { cpu: \"1\", memory: \"512Mi\" }"
echo " volumes:"
echo " - name: data"
echo " persistentVolumeClaim: { claimName: $pvc_name }"
end > $dply
echo "--- Deployment manifest ($dply) ---"
head -n 70 $dply
echo "..."
kubectl apply -f $dply; or return 1
if string match -qi 'y*' -- $want_np
_banner "Applying NetworkPolicy (deny-all ingress; allow all egress)"
set np (mktemp -t $app_name-netpol.XXXX.yaml)
begin
echo "apiVersion: networking.k8s.io/v1"
echo "kind: NetworkPolicy"
echo "metadata:"
printf " name: %s-deny-ingress\n" $app_name
echo " namespace: $ns"
echo "spec:"
echo " podSelector: { matchLabels: { app: $app_name } }"
echo " policyTypes: [\"Ingress\",\"Egress\"]"
echo " ingress: []"
echo " egress:"
echo " - {}"
end > $np
echo "--- NetworkPolicy manifest ($np) ---"
cat $np
kubectl apply -f $np; or return 1
end
2025-08-14 20:37:30 -05:00
_banner "Waiting for rollout"
2025-08-19 01:06:45 -05:00
if not kubectl -n $ns rollout status deploy/$app_name --timeout=300s
echo (set_color red)"Rollout did not finish in time. Recent events:"(set_color normal) >&2
kubectl -n $ns get events --sort-by=.lastTimestamp | tail -n 40
return 1
end
_banner "Validating Sui CLI inside the pod"
set ARCH (kubectl -n $ns exec deploy/$app_name -- sh -lc 'uname -m' 2>/dev/null)
set GLIBC (kubectl -n $ns exec deploy/$app_name -- sh -lc 'ldd --version 2>&1 | head -n1 || true' 2>/dev/null)
set SUIV (kubectl -n $ns exec deploy/$app_name -- sh -lc 'sui --version 2>&1 || true' 2>/dev/null)
echo "Node arch: $ARCH"
echo "GLIBC: $GLIBC"
echo "sui: $SUIV"
if test -z "$SUIV"; or string match -q "*not found*" -- $SUIV
echo (set_color red)"Error:"(set_color normal)" 'sui' did not run inside the pod."
echo "If you see GLIBC < 2.38 here, rebuild the image on Ubuntu 24.04 as shown in section A."
return 1
end
2025-08-14 20:37:30 -05:00
_banner "Initializing Sui client env ($net) and creating a new address"
2025-08-19 01:06:45 -05:00
# Create/switch env, then create a new address with alias.
# Try JSON first; fall back to parsing text.
set TMP (mktemp)
# Ensure config dir exists (inside the pod) and env points to your chosen RPC.
kubectl -n $ns exec deploy/$app_name -- sh -lc \
(printf 'mkdir -p /home/sui/.sui/sui_config ;
2025-08-14 20:37:30 -05:00
sui client new-env --alias %s --rpc %s >/dev/null 2>&1 || true ;
2025-08-19 01:06:45 -05:00
sui client switch --env %s >/dev/null 2>&1 || true' $net $rpc_url $net)
2025-08-14 20:37:30 -05:00
2025-08-19 01:06:45 -05:00
# Try JSON (preferred)
set JSON (kubectl -n $ns exec deploy/$app_name -- sh -lc \
(printf 'sui client new-address ed25519 --word-length %s --alias %s --json 2>/dev/null || true' $wl $alias))
set MN (printf "%s" $JSON | jq -r '.secretRecoveryPhrase // .mnemonic // .mnemonicPhrase // empty' 2>/dev/null)
set ADDR (printf "%s" $JSON | jq -r '.address // empty' 2>/dev/null)
if test -n "$MN"
kubectl -n $ns logs deploy/$app_name --since=10s >/dev/null 2>&1 || true
kubectl -n $ns exec deploy/$app_name -- sh -lc \
(printf 'echo "SECRET RECOVERY PHRASE: %s" >> /home/sui/README_MNEMONIC && chmod 0600 /home/sui/README_MNEMONIC' "$MN")
end
if test -z "$MN" -o -z "$ADDR"
# Fall back to plain text (robust across CLI changes)
kubectl -n $ns exec deploy/$app_name -- sh -lc \
(printf 'sui client new-address ed25519 --word-length %s --alias %s 2>&1 || true' $wl $alias) | tee $TMP >/dev/null
if test -z "$MN"
# Usually phrase is printed on the line right after the "Secret Recovery Phrase" header
set MN (awk '/Secret Recovery Phrase/ {getline; print; exit}' $TMP)
end
if test -z "$ADDR"
# Typical line: "Address: 0xabc..."; extract the RHS
set ADDR (awk -F': ' '/^Address:/ {print $2; exit}' $TMP)
end
end
# Make the generated address active (address wins; alias as backup)
if test -n "$ADDR"
kubectl -n $ns exec deploy/$app_name -- sh -lc (printf 'sui client switch --address %s >/dev/null 2>&1 || true' $ADDR)
else
kubectl -n $ns exec deploy/$app_name -- sh -lc (printf 'sui client switch --alias %s >/dev/null 2>&1 || true' $alias)
end
2025-08-14 20:37:30 -05:00
echo
echo (set_color yellow)"IMPORTANT — Secret Recovery Phrase (write down & store offline):"(set_color normal)
2025-08-19 01:06:45 -05:00
if test -n "$MN"
echo $MN
else
echo "(mnemonic not detected — check 'kubectl -n $ns logs deploy/$app_name' output)"
end
2025-08-14 20:37:30 -05:00
echo
echo "Address alias '$alias' appears to be: $ADDR"
2025-08-19 01:06:45 -05:00
# Optional: store mnemonic in a Secret (not recommended)
2025-08-14 20:37:30 -05:00
if test "$store_mn" = "y" -o "$store_mn" = "yes"
2025-08-19 01:06:45 -05:00
if test -n "$MN"
kubectl -n $ns create secret generic "$app_name-mnemonic" \
2025-08-14 20:37:30 -05:00
--from-literal=mnemonic="$MN" --dry-run=client -o yaml | kubectl -n $ns apply -f -
2025-08-19 01:06:45 -05:00
echo (set_color yellow)"Stored mnemonic in Secret $ns/{$app_name}-mnemonic. Treat your cluster as sensitive."(set_color normal)
2025-08-14 20:37:30 -05:00
else
2025-08-19 01:06:45 -05:00
echo (set_color red)"Warning:"(set_color normal)" mnemonic empty; not storing."
2025-08-14 20:37:30 -05:00
end
end
echo
echo "Done. Files inside the pod:"
2025-08-19 01:06:45 -05:00
kubectl -n $ns exec deploy/$app_name -- sh -lc 'ls -l /home/sui/.sui/sui_config || true'
2025-08-14 20:37:30 -05:00
echo
echo "Useful commands:"
2025-08-19 01:06:45 -05:00
echo " suiwallet_addresses $ns $app_name"
echo " suiwallet_gas $ns $app_name"
echo " suiwallet_export_keystore $ns $app_name ./sui.keystore.$base"
echo " suiwallet_metrics $ns $app_name $base"
echo " kubectl -n $ns exec -it deploy/$app_name -- sh # interactive CLI"
2025-08-14 20:37:30 -05:00
end
### --------- utilities ----------
2025-08-19 01:06:45 -05:00
function suiwallet_status -a NS APP
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
kubectl -n $NS get deploy,po,pvc,netpol -l app=$APP -o wide
end
function suiwallet_logs -a NS APP LINES
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
if test -z "$LINES"; set LINES 200; end
kubectl -n $NS logs deploy/$APP --tail=$LINES
end
function suiwallet_wallet_ls -a NS APP
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
echo "Listing /home/sui/.sui/sui_config in $NS/$APP" >&2
kubectl -n $NS exec deploy/$APP -- sh -lc 'ls -la /home/sui/.sui/sui_config || true'
end
2025-08-14 20:37:30 -05:00
function suiwallet_addresses -a NS APP
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
2025-08-19 01:06:45 -05:00
kubectl -n $NS exec deploy/$APP -- sh -lc 'sui client addresses || true'
2025-08-14 20:37:30 -05:00
end
function suiwallet_export_keystore -a NS APP DEST
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
if test -z "$DEST"; set DEST ./sui.keystore; end
2025-08-19 01:06:45 -05:00
set POD (kubectl -n $NS get pod -l app=$APP -o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
if test -z "$POD"
echo (set_color red)"Error:"(set_color normal)" no pod found for app=$APP in ns=$NS" >&2
return 1
end
echo "Copying keystore from $POD to $DEST (keep it safe!)"
kubectl -n $NS cp $POD:/home/sui/.sui/sui_config/sui.keystore $DEST
2025-08-14 20:37:30 -05:00
end
2025-08-19 01:06:45 -05:00
# Summarize gas coins and total balance; tolerant to schema differences
function suiwallet_gas -a NS APP
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
set J (kubectl -n $NS exec deploy/$APP -- sh -lc 'sui client gas --json' 2>/dev/null)
if test -z "$J"
echo "No JSON from 'sui client gas' (is the address set and env working?)" >&2
return 1
end
set NUM (printf "%s" $J | jq -r '[.. | .gasObjects? // empty | length] | add // 0')
if test -z "$NUM" -o "$NUM" = "null"
set NUM (printf "%s" $J | jq -r '[.. | .coins? // empty | length] | add // 0')
end
set SUM (printf "%s" $J | jq -r '
(.. | .totalBalance? // empty) as $t
| if $t != null then ($t|tonumber)
else [.. | .gasObjects? // empty | .[]? | .balance? | tonumber] | add
end // 0
')
# Convert Mist -> SUI using python -c
set SUM_SUI (python3 -c 'from decimal import Decimal; import sys; q=Decimal(sys.argv[1]) if sys.argv[1] else Decimal(0); print((q/Decimal("1e9")).quantize(Decimal("0.000000001")))' -- $SUM)
echo "Gas coins: $NUM Total: $SUM ($SUM_SUI SUI)"
end
# Wait until balance >= threshold (SUI), timeout seconds
function suiwallet_wait_balance -a NS APP MIN_SUI TIMEOUT
_need python3; or return 1
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
if test -z "$MIN_SUI"; set MIN_SUI 0; end
if test -z "$TIMEOUT"; set TIMEOUT 300; end
set start (date +%s)
while true
set J (kubectl -n $NS exec deploy/$APP -- sh -lc 'sui client gas --json' 2>/dev/null)
set SUM (printf "%s" $J | jq -r '
(.. | .totalBalance? // empty) as $t
| if $t != null then ($t|tonumber)
else [.. | .gasObjects? // empty | .[]? | .balance? | tonumber] | add
end // 0
')
set SUM_SUI (python3 -c 'from decimal import Decimal; import sys; q=Decimal(sys.argv[1]) if sys.argv[1] else Decimal(0); print(q/Decimal("1e9"))' -- $SUM)
set ge (python3 -c 'from decimal import Decimal; import sys; print(1 if Decimal(sys.argv[1]) >= Decimal(sys.argv[2]) else 0)' -- $SUM_SUI $MIN_SUI)
if test "$ge" = "1"
echo "Balance ready: $SUM_SUI SUI (>= $MIN_SUI)"
return 0
end
if test (math (date +%s) - $start) -ge $TIMEOUT
echo "Timed out waiting for balance >= $MIN_SUI SUI (current $SUM_SUI SUI)."
return 1
end
sleep 6
end
end
# Send SUI using `transfer-sui` picking the largest coin
# Usage: suiwallet_transfer_sui [ns] [app] <to_addr> <amount_sui> [gas_budget_mist=10000000]
function suiwallet_transfer_sui -a NS APP TO AMT_SUI GAS_BUDGET
_need jq python3; or return 1
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
if test -z "$TO" -o -z "$AMT_SUI"
echo "Usage: suiwallet_transfer_sui [ns] [app] <to_address> <amount_sui> [gas_budget_mist]" >&2
return 1
end
if test -z "$GAS_BUDGET"; set GAS_BUDGET 10000000; end
set AMT_MIST (python3 -c 'from decimal import Decimal; import sys; print(int((Decimal(sys.argv[1])*Decimal("1e9")).to_integral_value()))' -- $AMT_SUI)
set J (kubectl -n $NS exec deploy/$APP -- sh -lc 'sui client gas --json' 2>/dev/null)
set COIN (printf "%s" $J | jq -r '
([.. | .gasObjects? // empty | .[]?] // [])
| sort_by(.balance|tonumber) | last | .coinObjectId // empty
')
if test -z "$COIN"
echo (set_color red)"No gas coins found to send from."(set_color normal) >&2
return 1
end
echo "Using coin: $COIN"
kubectl -n $NS exec deploy/$APP -- sh -lc \
(printf 'sui client transfer-sui --to %s --sui-coin-object-id %s --amount %s --gas-budget %s' $TO $COIN $AMT_MIST $GAS_BUDGET)
end
# Merge all gas coins into the largest one
function suiwallet_merge_gas -a NS APP
_need jq; or return 1
2025-08-14 20:37:30 -05:00
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
2025-08-19 01:06:45 -05:00
set J (kubectl -n $NS exec deploy/$APP -- sh -lc 'sui client gas --json' 2>/dev/null)
set IDS (printf "%s" $J | jq -r '([.. | .gasObjects? // empty | .[]?] // []) | sort_by(.balance|tonumber) | reverse | .[].coinObjectId')
set coins $IDS
if test (count $coins) -lt 2
echo "Nothing to merge (need >=2 gas coins)."
return 0
end
set primary $coins[1]
for i in (seq 2 (count $coins))
set c $coins[$i]
echo "Merging $c -> $primary"
kubectl -n $NS exec deploy/$APP -- sh -lc (printf 'sui client merge-coin --primary-coin %s --coin-to-merge %s --gas-budget 10000000' $primary $c)
end
end
# Restore an address from a Secret Recovery Phrase into the running pod
# Usage: suiwallet_restore [ns] [app] [alias] [network=mainnet]
function suiwallet_restore -a NS APP ALIAS NET
_need kubectl jq; or return 1
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
if test -z "$ALIAS"; set ALIAS restored; end
if test -z "$NET"; set NET mainnet; end
switch $NET
case mainnet
set RPC https://fullnode.mainnet.sui.io:443
case testnet
set RPC https://fullnode.testnet.sui.io:443
case devnet
set RPC https://fullnode.devnet.sui.io:443
case '*'
echo (set_color red)"Error:"(set_color normal)" NET must be mainnet|testnet|devnet"; return 1
end
echo (set_color yellow)"Paste your 12/24-word Sui Secret Recovery Phrase (input hidden):"(set_color normal)
read -s MNEMONIC
echo
if test -z "$MNEMONIC"
echo (set_color red)"Error:"(set_color normal)" empty mnemonic."; return 1
end
# Ensure env is configured and active
kubectl -n $NS exec deploy/$APP -- sh -lc \
(printf 'mkdir -p /home/sui/.sui/sui_config ;
sui client new-env --alias %s --rpc %s >/dev/null 2>&1 || true ;
sui client switch --env %s >/dev/null 2>&1 || true' $NET $RPC $NET); or return 1
# Import mnemonic into keystore (non-interactive; enter scheme then phrase)
# keytool is interactive; we feed "ed25519" then the phrase via stdin.
set TMP (mktemp)
printf "ed25519\n%s\n" "$MNEMONIC" \
| kubectl -n $NS exec -i deploy/$APP -- sh -lc 'sui keytool import 2>&1' \
| tee $TMP >/dev/null
# Create a named alias pointing to one of the imported keys by re-using new-address --recover
# (new-address --recover will prompt for phrase; we feed it again to bind an alias)
set OUT (mktemp)
printf "%s\n" "$MNEMONIC" \
| kubectl -n $NS exec -i deploy/$APP -- sh -lc \
(printf 'sui client new-address ed25519 --alias %s --recover 2>&1' $ALIAS) \
| tee $OUT >/dev/null
# Try to extract the resulting address
set ADDR (awk -F': ' '/^Address:/ {print $2; exit}' $OUT)
if test -z "$ADDR"
# fallback: pick the address for the alias from the address table
set ADDR (kubectl -n $NS exec deploy/$APP -- sh -lc (printf 'sui client addresses | awk "/ %s\\s*$/ {print \\$NF}" | tail -n1' $ALIAS) 2>/dev/null)
end
echo
echo "Restored alias: $ALIAS"
echo "Address: $ADDR"
echo (set_color yellow)"IMPORTANT:"(set_color normal)" store your recovery phrase offline. Do NOT keep it in the cluster."
# Make it active for the client
if test -n "$ADDR"
kubectl -n $NS exec deploy/$APP -- sh -lc (printf 'sui client switch --address %s >/dev/null 2>&1 || true' $ADDR)
end
2025-08-14 20:37:30 -05:00
end
function suiwallet_stop -a NS APP
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
kubectl -n $NS delete deploy $APP --ignore-not-found
2025-08-19 01:06:45 -05:00
echo "Stopped $NS/$APP. PVC retained."
end
# Print the primary (first) address by alias search
function suiwallet_primary_address -a NS APP
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
kubectl -n $NS exec deploy/$APP -- sh -lc \
"sui client addresses | awk 'NF{print \$NF}' | head -n1" 2>/dev/null
end
# Quick overview
function suiwallet_overview -a NS APP
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
echo "== Sui wallet overview (ns=$NS app=$APP)"
kubectl -n $NS exec deploy/$APP -- sh -lc 'echo "-- Addresses:"; sui client addresses || true'
kubectl -n $NS exec deploy/$APP -- sh -lc 'echo "-- Gas objects:"; sui client gas 2>/dev/null || true'
end
# Sui keystore size (for Grafana/Prometheus)
function suiwallet_keystore_size_bytes -a NS APP
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
set SZ (kubectl -n $NS exec deploy/$APP -- sh -lc \
'test -e /home/sui/.sui/sui_config/sui.keystore && stat -c %s /home/sui/.sui/sui_config/sui.keystore || echo 0' 2>/dev/null)
echo $SZ
end
# Prometheus-friendly one-liner metric
# Example usage: suiwallet_metrics crypto wallet-sui-test mywallet
function suiwallet_metrics -a NS APP WALLET_NAME
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
if test -z "$WALLET_NAME"; set WALLET_NAME main; end
set BYTES (suiwallet_keystore_size_bytes $NS $APP)
printf "wallet_size_bytes{chain=\"sui\",namespace=\"%s\",app=\"%s\",wallet=\"%s\"} %s\n" $NS $APP $WALLET_NAME $BYTES
2025-08-14 20:37:30 -05:00
end
function suiwallet_purge -a NS APP PVC
if test -z "$NS"; set NS crypto; end
if test -z "$APP"; set APP wallet-sui-main; end
if test -z "$PVC"; set PVC $APP; end
echo "Type to confirm: PURGE $NS $PVC"
read CONFIRM
if test "$CONFIRM" != "PURGE $NS $PVC"
2025-08-19 01:06:45 -05:00
echo "Aborted."
return 1
2025-08-14 20:37:30 -05:00
end
kubectl -n $NS delete deploy $APP --ignore-not-found
kubectl -n $NS delete pvc $PVC --ignore-not-found --wait=true
2025-08-19 01:06:45 -05:00
kubectl -n $NS delete netpol (printf "%s-deny-ingress" $APP) --ignore-not-found >/dev/null 2>&1
echo "Purged. Ensure you have the mnemonic backed up if you ever need to restore."
end
function suiwallet_help
echo "suiwallet_bootstrap # interactive deploy + PVC + optional NP; creates env+address, prints mnemonic"
echo "suiwallet_status [ns] [app] # show deploy/pods/pvc/netpol"
echo "suiwallet_logs [ns] [app] [N] # tail N (default 200) lines of pod logs"
echo "suiwallet_wallet_ls [ns] [app] # list ~/.sui/sui_config inside pod"
echo "suiwallet_addresses [ns] [app] # list addresses"
echo "suiwallet_gas [ns] [app] # summarize gas coins and total (Mist & SUI)"
echo "suiwallet_wait_balance [ns] [app] <min_sui> [timeout_s=300]"
echo "suiwallet_transfer_sui [ns] [app] <to> <amount_sui> [gas_budget_mist]"
echo "suiwallet_merge_gas [ns] [app] # merge many gas coins into one"
echo "suiwallet_export_keystore [ns] [app] [dest=./sui.keystore]"
echo "suiwallet_metrics [ns] [app] [walletname] # Prometheus line: wallet_size_bytes{chain=\"sui\",...}"
echo "suiwallet_stop [ns] [app] # delete deployment (keep PVC)"
echo "suiwallet_purge [ns] [app] [pvc] # delete deployment + PVC (danger!)"
2025-08-14 20:37:30 -05:00
end