Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"tiny-secp256k1": "./node_modules/@tapylet/core/dist/lib/secp256k1-compat.js"
},
"dependencies": {
"@tapylet/core": "^0.0.7",
"@tapylet/core": "^0.0.8",
"@plasmohq/storage": "^1.15.0",
"i18next": "^25.8.13",
"i18next-browser-languagedetector": "^8.2.1",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src/extension/components/wallet/AssetDetailModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export const AssetDetailModal: React.FC<AssetDetailModalProps> = ({
amount: parsedAmount,
colorId,
mnemonic: walletData.mnemonic,
networkId: network.id,
})

setBurnTxid(result.txid)
Expand Down
1 change: 1 addition & 0 deletions src/extension/components/wallet/IssueModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ export const IssueModal: React.FC<IssueModalProps> = ({
metadata,
mnemonic: walletData.mnemonic,
fromAddress: address,
networkId: network.id,
})

// Save to local storage
Expand Down
20 changes: 18 additions & 2 deletions src/extension/components/wallet/SendModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,15 @@ import { isValidAmount, parseAndValidateAmount, MAX_AMOUNT, MAX_COLORED_AMOUNT }

interface SendModalProps {
address: string
// TIP-0044 id of the network `address` (and any recipient address) is
// encoded for.
networkId: number
// Sign with the pre-network-split key instead of deriving one for
// networkId (see @tapylet/core/wallet/transaction#SendOptions). Only used
// by the legacy-address recovery screen.
fromLegacyMainnetWallet?: boolean
// Prefilled into the recipient field each time the modal opens; still editable.
defaultToAddress?: string
tpcBalance: BalanceDetails
assets: AssetBalance[]
tokenMetadata: Map<string, Metadata>
Expand All @@ -22,6 +31,9 @@ type SendStep = "input" | "confirm" | "sending" | "success" | "error"

export const SendModal: React.FC<SendModalProps> = ({
address,
networkId,
fromLegacyMainnetWallet,
defaultToAddress = "",
tpcBalance,
assets,
tokenMetadata,
Expand All @@ -32,7 +44,7 @@ export const SendModal: React.FC<SendModalProps> = ({
const { t } = useTranslation()
const [step, setStep] = useState<SendStep>("input")
const [selectedColorId, setSelectedColorId] = useState<string>(TPC_COLOR_ID)
const [toAddress, setToAddress] = useState("")
const [toAddress, setToAddress] = useState(defaultToAddress)
const [amount, setAmount] = useState("")
const [split, setSplit] = useState("1")
const [error, setError] = useState<string | null>(null)
Expand All @@ -50,7 +62,7 @@ export const SendModal: React.FC<SendModalProps> = ({
const resetState = () => {
setStep("input")
setSelectedColorId(TPC_COLOR_ID)
setToAddress("")
setToAddress(defaultToAddress)
setAmount("")
setSplit("1")
setError(null)
Expand Down Expand Up @@ -186,6 +198,8 @@ export const SendModal: React.FC<SendModalProps> = ({
amount: sendAmount,
split: sendSplit,
mnemonic: walletData.mnemonic,
networkId,
fromLegacyMainnetWallet,
})
} else {
sendAmount = parseAndValidateAmount(amount, MAX_COLORED_AMOUNT, selectedDecimals)!
Expand All @@ -196,6 +210,8 @@ export const SendModal: React.FC<SendModalProps> = ({
split: sendSplit,
colorId: selectedColorId,
mnemonic: walletData.mnemonic,
networkId,
fromLegacyMainnetWallet,
})
}

Expand Down
30 changes: 27 additions & 3 deletions src/extension/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"tryAgain": "Try Again",
"copy": "Copy",
"copied": "Copied!",
"decimalHint": "Up to {{decimals}} decimal places"
"decimalHint": "Up to {{decimals}} decimal places",
"settings": "Settings"
},
"network": {
"mainnet": "Mainnet",
Expand Down Expand Up @@ -127,7 +128,8 @@
"password": "Password",
"unlock": "Unlock",
"errors": {
"incorrectPassword": "Incorrect password"
"incorrectPassword": "Incorrect password",
"walletUpdateFailed": "Failed to update the wallet data. Please unlock again."
}
},
"assetDetail": {
Expand Down Expand Up @@ -178,7 +180,29 @@
"autoLockMinutes_other": "{{count}} minutes",
"autoLockHours_one": "{{count}} hour",
"autoLockHours_other": "{{count}} hours",
"autoLockNever": "Never"
"autoLockNever": "Never",
"legacyAddressTitle": "View the mainnet address from the older version",
"legacyAddressButton": "View"
},
"legacyAddress": {
"title": "Previous mainnet address",
"description": "This is the mainnet address used by the older version. If anything remains here, you can send it to your new mainnet address.",
"address": "Address",
"switchToMainnet": "Switch to mainnet to check this address's balance.",
"switchButton": "Switch to mainnet",
"sendButton": "Send to the new address"
},
"missingNetworkKey": {
"title": "No key generated for {{network}} yet",
"description": "Key generation failed for some reason. Please try again.",
"regenerate": "Generate key",
"error": "Failed to generate the key. Please try again."
},
"legacyMigrationNotice": {
"title": "Your mainnet address has changed",
"description": "You can view and send from your previous mainnet address under Settings → \"View the mainnet address from the older version\".",
"openSettings": "Open settings",
"continueButton": "Continue"
},
"issue": {
"title": "Issue Asset",
Expand Down
30 changes: 27 additions & 3 deletions src/extension/i18n/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"tryAgain": "再試行",
"copy": "コピー",
"copied": "コピーしました",
"decimalHint": "小数点第{{decimals}}位まで入力可能"
"decimalHint": "小数点第{{decimals}}位まで入力可能",
"settings": "設定"
},
"network": {
"mainnet": "メインネット",
Expand Down Expand Up @@ -127,7 +128,8 @@
"password": "パスワード",
"unlock": "ロック解除",
"errors": {
"incorrectPassword": "パスワードが正しくありません"
"incorrectPassword": "パスワードが正しくありません",
"walletUpdateFailed": "ウォレットデータの更新に失敗しました。もう一度ロック解除してください"
}
},
"assetDetail": {
Expand Down Expand Up @@ -178,7 +180,29 @@
"autoLockMinutes_other": "{{count}} 分",
"autoLockHours_one": "{{count}} 時間",
"autoLockHours_other": "{{count}} 時間",
"autoLockNever": "ロックしない"
"autoLockNever": "ロックしない",
"legacyAddressTitle": "古いバージョンのメインネットアドレスを確認する",
"legacyAddressButton": "確認する"
},
"legacyAddress": {
"title": "以前のメインネットアドレス",
"description": "古いバージョンで使用していたメインネットのアドレスです。ここに資産が残っている場合、新しいメインネットアドレスへ送金して移せます。",
"address": "アドレス",
"switchToMainnet": "このアドレスの残高を確認するには、メインネットに切り替えてください。",
"switchButton": "メインネットに切り替える",
"sendButton": "新しいアドレスへ送金する"
},
"missingNetworkKey": {
"title": "{{network}}用の鍵が未生成です",
"description": "何らかの理由で鍵の生成に失敗しました。もう一度お試しください。",
"regenerate": "鍵を生成する",
"error": "鍵の生成に失敗しました。もう一度お試しください。"
},
"legacyMigrationNotice": {
"title": "メインネットのアドレスが変わりました",
"description": "以前のメインネットアドレスは設定画面の「古いバージョンのメインネットアドレスを確認する」から確認・送金できます。",
"openSettings": "設定を開く",
"continueButton": "続ける"
},
"issue": {
"title": "アセット発行",
Expand Down
166 changes: 166 additions & 0 deletions src/extension/screens/LegacyAddressScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import React, { useCallback, useEffect, useState } from "react"
import { useTranslation } from "react-i18next"
import { Button, Card, CardContent, Loading } from "../components/ui"
import { AddressDisplay, SendModal } from "../components/wallet"
import { NetworkId } from "@tapylet/core/wallet"
import {
getAllBalances,
formatTpc,
formatTokenAmount,
formatColorId,
getTokenMetadataBatch,
Metadata,
type AllBalances,
} from "@tapylet/core/api"
import { pendingTxStore } from "~/extension/storage"
import { useNetwork } from "~/extension/hooks/useNetwork"
import type { AppScreen } from "~/extension/types/navigation"

interface LegacyAddressScreenProps {
legacyMainnetAddress: string
// Where funds left at the legacy address are expected to go. Null when the
// mainnet key could not be derived, in which case nothing is prefilled.
newMainnetAddress: string | null
onNavigate: (screen: AppScreen) => void
}

// The pre-network-split address was always mainnet-formatted (see
// @tapylet/core/wallet/hdwallet#getKeyPairFromLegacyMainnetWallet), so its
// balance can only be read while the panel is pointed at the mainnet
// explorer. Rather than juggling a second, temporary network configuration,
// this screen simply asks for mainnet to be selected first.
export const LegacyAddressScreen: React.FC<LegacyAddressScreenProps> = ({
legacyMainnetAddress,
newMainnetAddress,
onNavigate,
}) => {
const { t } = useTranslation()
const { network, switchNetwork } = useNetwork()
const [balances, setBalances] = useState<AllBalances | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [loadError, setLoadError] = useState<string | null>(null)
const [tokenMetadata, setTokenMetadata] = useState<Map<string, Metadata>>(new Map())
const [showSendModal, setShowSendModal] = useState(false)

const isOnMainnet = network.key === "mainnet"

const refresh = useCallback(async () => {
if (!isOnMainnet) return
setIsLoading(true)
setLoadError(null)
try {
const allBal = await getAllBalances(legacyMainnetAddress)
setBalances(allBal)
if (allBal.assets.length > 0) {
const meta = await getTokenMetadataBatch(allBal.assets.map((a) => a.colorId))
setTokenMetadata(meta)
}
} catch (err) {
console.error("Failed to load the legacy address balance:", err)
setLoadError(t("wallet.failedToLoad"))
} finally {
setIsLoading(false)
}
}, [isOnMainnet, legacyMainnetAddress, t])

useEffect(() => {
refresh()
}, [refresh])

return (
<div className="flex flex-col h-full">
<div className="bg-slate-700 text-white p-6">
<div className="flex items-center gap-3">
<button
onClick={() => onNavigate("settings")}
className="p-2 -ml-2 hover:bg-white/10 rounded-lg transition-colors">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<h1 className="text-lg font-semibold">{t("legacyAddress.title")}</h1>
</div>
</div>

<div className="flex-1 p-6 space-y-4">
<p className="text-sm text-slate-600">{t("legacyAddress.description")}</p>

<Card>
<CardContent>
<p className="text-sm font-medium text-slate-700 mb-3">{t("legacyAddress.address")}</p>
<AddressDisplay address={legacyMainnetAddress} showFull />
</CardContent>
</Card>

{!isOnMainnet ? (
<Card>
<CardContent>
<p className="text-sm text-slate-600 mb-3">{t("legacyAddress.switchToMainnet")}</p>
<Button fullWidth onClick={() => switchNetwork("mainnet").catch((err) => console.error(err))}>
{t("legacyAddress.switchButton")}
</Button>
</CardContent>
</Card>
) : (
<Card>
<CardContent>
<p className="text-sm font-medium text-slate-700 mb-3">{t("wallet.totalBalance")}</p>
{isLoading ? (
<Loading size="sm" />
) : loadError ? (
<p className="text-sm text-red-500">{loadError}</p>
) : (
<>
<p className="text-2xl font-bold text-slate-800">
{formatTpc(balances?.tpc.total ?? 0)} TPC
</p>
{balances && balances.assets.length > 0 && (
<div className="mt-3 space-y-2">
{balances.assets.map((asset) => {
const meta = tokenMetadata.get(asset.colorId)
return (
<div
key={asset.colorId}
className="flex items-center justify-between p-2 bg-slate-50 rounded-lg">
<span className="text-xs font-mono text-slate-600 truncate">
{meta ? `${meta.name} (${meta.symbol})` : formatColorId(asset.colorId)}
</span>
<span className="text-sm font-semibold text-slate-800">
{formatTokenAmount(asset.total, meta?.decimals)}
</span>
</div>
)
})}
</div>
)}
<Button fullWidth className="mt-4" onClick={() => setShowSendModal(true)}>
{t("legacyAddress.sendButton")}
</Button>
</>
)}
</CardContent>
</Card>
)}
</div>

<SendModal
address={legacyMainnetAddress}
networkId={NetworkId.TAPYRUS_API}
fromLegacyMainnetWallet
defaultToAddress={newMainnetAddress ?? undefined}
tpcBalance={balances?.tpc ?? { confirmed: 0, unconfirmed: 0, total: 0 }}
assets={balances?.assets ?? []}
tokenMetadata={tokenMetadata}
isOpen={showSendModal}
onClose={() => setShowSendModal(false)}
onSuccess={async (txid, amount, toAddress, colorId) => {
await pendingTxStore.add({ txid, amount, toAddress, timestamp: Date.now(), colorId })
for (let i = 0; i < 5; i++) {
await new Promise((resolve) => setTimeout(resolve, 2000))
await refresh()
}
}}
/>
</div>
)
}
Loading
Loading