mirror of
https://github.com/sonr-io/sonr.git
synced 2026-08-03 01:41:44 +00:00
feature/1220 origin handle exists method (#1241)
* feat: add docs and CI workflow for publishing to onsonr.dev * (refactor): Move hway,motr executables to their own repos * feat: simplify devnet and testnet configurations * refactor: update import path for didcrypto package * docs(networks): Add README with project overview, architecture, and community links * refactor: Move network configurations to deploy directory * build: update golang version to 1.23 * refactor: move logger interface to appropriate package * refactor: Move devnet configuration to networks/devnet * chore: improve release process with date variable * (chore): Move Crypto Library * refactor: improve code structure and readability in DID module * feat: integrate Trunk CI checks * ci: optimize CI workflow by removing redundant build jobs --------- Co-authored-by: Darp Alakun <i@prad.nu>
This commit is contained in:
@@ -9,6 +9,7 @@ The DID module maintains several key state structures:
|
||||
### Controller State
|
||||
|
||||
The Controller state represents a Sonr DWN Vault. It includes:
|
||||
|
||||
- Unique identifier (number)
|
||||
- DID
|
||||
- Sonr address
|
||||
@@ -22,6 +23,7 @@ The Controller state represents a Sonr DWN Vault. It includes:
|
||||
### Assertion State
|
||||
|
||||
The Assertion state includes:
|
||||
|
||||
- DID
|
||||
- Controller
|
||||
- Subject
|
||||
@@ -33,6 +35,7 @@ The Assertion state includes:
|
||||
### Authentication State
|
||||
|
||||
The Authentication state includes:
|
||||
|
||||
- DID
|
||||
- Controller
|
||||
- Subject
|
||||
@@ -44,6 +47,7 @@ The Authentication state includes:
|
||||
### Verification State
|
||||
|
||||
The Verification state includes:
|
||||
|
||||
- DID
|
||||
- Controller
|
||||
- DID method
|
||||
@@ -57,6 +61,7 @@ The Verification state includes:
|
||||
## State Transitions
|
||||
|
||||
State transitions are triggered by the following messages:
|
||||
|
||||
- LinkAssertion
|
||||
- LinkAuthentication
|
||||
- UnlinkAssertion
|
||||
@@ -89,6 +94,7 @@ The DID module provides the following query endpoints:
|
||||
## Params
|
||||
|
||||
The module parameters include:
|
||||
|
||||
- Allowed public keys (map of KeyInfo)
|
||||
- Conveyance preference
|
||||
- Attestation formats
|
||||
@@ -123,6 +129,7 @@ This module utilizes UCAN (User Controlled Authorization Networks) to provide a
|
||||
## Future Improvements
|
||||
|
||||
Potential future improvements could include:
|
||||
|
||||
1. Enhanced privacy features for DID operations, potentially leveraging UCAN capabilities for privacy-preserving authorization.
|
||||
2. Integration with more blockchain networks
|
||||
3. Support for additional key types and cryptographic algorithms
|
||||
@@ -131,6 +138,7 @@ Potential future improvements could include:
|
||||
## Tests
|
||||
|
||||
Acceptance tests should cover all major functionality, including:
|
||||
|
||||
- Creating and managing DIDs
|
||||
- Linking and unlinking assertions and authentications
|
||||
- Executing transactions with DIDs
|
||||
@@ -154,6 +162,7 @@ A Verifiable Credential (VC) is a digital statement that can be cryptographicall
|
||||
### Key Types
|
||||
|
||||
The module supports various key types, including:
|
||||
|
||||
- Role
|
||||
- Algorithm (e.g., ES256, EdDSA, ES256K)
|
||||
- Encoding (e.g., hex, base64, multibase)
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
"github.com/cosmos/cosmos-sdk/types/address"
|
||||
|
||||
"github.com/onsonr/sonr/internal/chain/transaction"
|
||||
"github.com/onsonr/sonr/x/did/types/internal/accounts"
|
||||
"github.com/onsonr/sonr/internal/accounts"
|
||||
"github.com/onsonr/sonr/internal/transaction"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
package address
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha512"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"math/big"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec/v2"
|
||||
)
|
||||
|
||||
// ComputePublicKey computes the public key of a child key given the extended public key, chain code, coin type, and index.
|
||||
func ComputePublicKey(extPubKey []byte, chainCode []byte, coinType uint32, index int) ([]byte, error) {
|
||||
// Check if the index is a hardened child key
|
||||
if uint32(index) >= HardenedOffset {
|
||||
return nil, errors.New("cannot derive hardened child key from public key")
|
||||
}
|
||||
|
||||
// Serialize the public key
|
||||
pubKey, err := btcec.ParsePubKey(extPubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
pubKeyBytes := pubKey.SerializeCompressed()
|
||||
|
||||
// Serialize the index
|
||||
indexBytes := make([]byte, 4)
|
||||
binary.BigEndian.PutUint32(indexBytes, uint32(index))
|
||||
|
||||
// Compute the HMAC-SHA512
|
||||
mac := hmac.New(sha512.New, chainCode)
|
||||
mac.Write(pubKeyBytes)
|
||||
mac.Write(indexBytes)
|
||||
I := mac.Sum(nil)
|
||||
|
||||
// Split I into two 32-byte sequences
|
||||
IL := I[:32]
|
||||
|
||||
// Convert IL to a big integer
|
||||
ilNum := new(big.Int).SetBytes(IL)
|
||||
|
||||
// Check if parse256(IL) >= n
|
||||
curve := btcec.S256()
|
||||
if ilNum.Cmp(curve.N) >= 0 {
|
||||
return nil, errors.New("invalid child key")
|
||||
}
|
||||
|
||||
// Compute the child public key: pubKey + IL * G
|
||||
ilx, ily := curve.ScalarBaseMult(IL)
|
||||
childX, childY := curve.Add(ilx, ily, pubKey.X(), pubKey.Y())
|
||||
lx := newBigIntFieldVal(childX)
|
||||
ly := newBigIntFieldVal(childY)
|
||||
|
||||
// Create the child public key
|
||||
childPubKey := btcec.NewPublicKey(lx, ly)
|
||||
childPubKeyBytes := childPubKey.SerializeCompressed()
|
||||
return childPubKeyBytes, nil
|
||||
}
|
||||
|
||||
// newBigIntFieldVal creates a new field value from a big integer.
|
||||
func newBigIntFieldVal(val *big.Int) *btcec.FieldVal {
|
||||
lx := new(btcec.FieldVal)
|
||||
lx.SetByteSlice(val.Bytes())
|
||||
return lx
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package address
|
||||
|
||||
type CoinType uint32
|
||||
|
||||
const (
|
||||
// Hardened offset for BIP-44 derivation
|
||||
HardenedOffset uint32 = 0x80000000
|
||||
|
||||
// Registered coin types for BIP-44
|
||||
CoinTypeBitcoin CoinType = CoinType(0 + HardenedOffset)
|
||||
CoinTypeEthereum CoinType = CoinType(60 + HardenedOffset)
|
||||
CoinTypeSonr CoinType = CoinType(703 + HardenedOffset)
|
||||
)
|
||||
|
||||
// Uint32 returns the coin type as a uint32.
|
||||
func (c CoinType) Uint32() uint32 {
|
||||
return uint32(c)
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
||||
"github.com/multiformats/go-multicodec"
|
||||
"github.com/multiformats/go-varint"
|
||||
)
|
||||
|
||||
// GenerateEd25519 generates an Ed25519 private key and the matching DID.
|
||||
// This is the RECOMMENDED algorithm.
|
||||
func GenerateEd25519() (crypto.PrivKey, DID, error) {
|
||||
priv, pub, err := crypto.GenerateEd25519Key(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, Undef, nil
|
||||
}
|
||||
did, err := FromPubKey(pub)
|
||||
return priv, did, err
|
||||
}
|
||||
|
||||
// GenerateRSA generates a RSA private key and the matching DID.
|
||||
func GenerateRSA() (crypto.PrivKey, DID, error) {
|
||||
// NIST Special Publication 800-57 Part 1 Revision 5
|
||||
// Section 5.6.1.1 (Table 2)
|
||||
// Paraphrased: 2048-bit RSA keys are secure until 2030 and 3072-bit keys are recommended for longer-term security.
|
||||
const keyLength = 3072
|
||||
|
||||
priv, pub, err := crypto.GenerateRSAKeyPair(keyLength, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, Undef, nil
|
||||
}
|
||||
did, err := FromPubKey(pub)
|
||||
return priv, did, err
|
||||
}
|
||||
|
||||
// GenerateSecp256k1 generates a Secp256k1 private key and the matching DID.
|
||||
func GenerateSecp256k1() (crypto.PrivKey, DID, error) {
|
||||
priv, pub, err := crypto.GenerateSecp256k1Key(rand.Reader)
|
||||
if err != nil {
|
||||
return nil, Undef, nil
|
||||
}
|
||||
did, err := FromPubKey(pub)
|
||||
return priv, did, err
|
||||
}
|
||||
|
||||
// GenerateECDSA generates an ECDSA private key and the matching DID
|
||||
// for the default P256 curve.
|
||||
func GenerateECDSA() (crypto.PrivKey, DID, error) {
|
||||
return GenerateECDSAWithCurve(P256)
|
||||
}
|
||||
|
||||
// GenerateECDSAWithCurve generates an ECDSA private key and matching
|
||||
// DID for the user-supplied curve
|
||||
func GenerateECDSAWithCurve(code multicodec.Code) (crypto.PrivKey, DID, error) {
|
||||
var curve elliptic.Curve
|
||||
|
||||
switch code {
|
||||
case P256:
|
||||
curve = elliptic.P256()
|
||||
case P384:
|
||||
curve = elliptic.P384()
|
||||
case P521:
|
||||
curve = elliptic.P521()
|
||||
default:
|
||||
return nil, Undef, errors.New("unsupported ECDSA curve")
|
||||
}
|
||||
|
||||
priv, pub, err := crypto.GenerateECDSAKeyPairWithCurve(curve, rand.Reader)
|
||||
if err != nil {
|
||||
return nil, Undef, err
|
||||
}
|
||||
|
||||
did, err := FromPubKey(pub)
|
||||
|
||||
return priv, did, err
|
||||
}
|
||||
|
||||
// FromPrivKey is a convenience function that returns the DID associated
|
||||
// with the public key associated with the provided private key.
|
||||
func FromPrivKey(privKey crypto.PrivKey) (DID, error) {
|
||||
return FromPubKey(privKey.GetPublic())
|
||||
}
|
||||
|
||||
// FromPubKey returns a did:key constructed from the provided public key.
|
||||
func FromPubKey(pubKey crypto.PubKey) (DID, error) {
|
||||
var code multicodec.Code
|
||||
|
||||
switch pubKey.Type() {
|
||||
case pb.KeyType_Ed25519:
|
||||
code = multicodec.Ed25519Pub
|
||||
case pb.KeyType_RSA:
|
||||
code = RSA
|
||||
case pb.KeyType_Secp256k1:
|
||||
code = Secp256k1
|
||||
case pb.KeyType_ECDSA:
|
||||
var err error
|
||||
if code, err = codeForCurve(pubKey); err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
default:
|
||||
return Undef, errors.New("unsupported key type")
|
||||
}
|
||||
|
||||
if pubKey.Type() == pb.KeyType_ECDSA && code == Secp256k1 {
|
||||
var err error
|
||||
|
||||
pubKey, err = coerceECDSAToSecp256k1(pubKey)
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
}
|
||||
|
||||
var bytes []byte
|
||||
|
||||
switch pubKey.Type() {
|
||||
case pb.KeyType_ECDSA:
|
||||
pkix, err := pubKey.Raw()
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
|
||||
publicKey, err := x509.ParsePKIXPublicKey(pkix)
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
|
||||
ecdsaPublicKey := publicKey.(*ecdsa.PublicKey)
|
||||
|
||||
bytes = elliptic.MarshalCompressed(ecdsaPublicKey.Curve, ecdsaPublicKey.X, ecdsaPublicKey.Y)
|
||||
case pb.KeyType_Ed25519, pb.KeyType_Secp256k1:
|
||||
var err error
|
||||
|
||||
if bytes, err = pubKey.Raw(); err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
case pb.KeyType_RSA:
|
||||
var err error
|
||||
|
||||
pkix, err := pubKey.Raw()
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
|
||||
publicKey, err := x509.ParsePKIXPublicKey(pkix)
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
|
||||
bytes = x509.MarshalPKCS1PublicKey(publicKey.(*rsa.PublicKey))
|
||||
}
|
||||
|
||||
return DID{
|
||||
code: code,
|
||||
bytes: string(append(varint.ToUvarint(uint64(code)), bytes...)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ToPubKey returns the crypto.PubKey encapsulated in the DID formed by
|
||||
// parsing the provided string.
|
||||
func ToPubKey(s string) (crypto.PubKey, error) {
|
||||
id, err := Parse(s)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return id.PubKey()
|
||||
}
|
||||
|
||||
func codeForCurve(pubKey crypto.PubKey) (multicodec.Code, error) {
|
||||
stdPub, err := crypto.PubKeyToStdKey(pubKey)
|
||||
if err != nil {
|
||||
return multicodec.Identity, err
|
||||
}
|
||||
|
||||
ecdsaPub, ok := stdPub.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return multicodec.Identity, errors.New("failed to assert type for code to curve")
|
||||
}
|
||||
|
||||
switch ecdsaPub.Curve {
|
||||
case elliptic.P256():
|
||||
return P256, nil
|
||||
case elliptic.P384():
|
||||
return P384, nil
|
||||
case elliptic.P521():
|
||||
return P521, nil
|
||||
case secp256k1.S256():
|
||||
return Secp256k1, nil
|
||||
default:
|
||||
return multicodec.Identity, fmt.Errorf("unsupported ECDSA curve: %s", ecdsaPub.Curve.Params().Name)
|
||||
}
|
||||
}
|
||||
|
||||
// secp256k1.S256 is a valid ECDSA curve, but the go-libp2p/core/crypto
|
||||
// package treats it as a different type and has a different format for
|
||||
// the raw bytes of the public key.
|
||||
//
|
||||
// If a valid ECDSA public key was created using the secp256k1.S256 curve,
|
||||
// this function will "convert" it from a crypto.ECDSAPubKey to a
|
||||
// crypto.Secp256k1PublicKey.
|
||||
func coerceECDSAToSecp256k1(pubKey crypto.PubKey) (crypto.PubKey, error) {
|
||||
stdPub, err := crypto.PubKeyToStdKey(pubKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ecdsaPub, ok := stdPub.(*ecdsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, errors.New("failed to assert type for secp256k1 coersion")
|
||||
}
|
||||
|
||||
ecdsaPubBytes := append([]byte{0x04}, append(ecdsaPub.X.Bytes(), ecdsaPub.Y.Bytes()...)...)
|
||||
|
||||
secp256k1Pub, err := secp256k1.ParsePubKey(ecdsaPubBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cryptoPub := crypto.Secp256k1PublicKey(*secp256k1Pub)
|
||||
|
||||
return &cryptoPub, nil
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package crypto_test
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
libp2p_crypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||
"github.com/libp2p/go-libp2p/core/crypto/pb"
|
||||
"github.com/multiformats/go-multicodec"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/onsonr/sonr/x/did/types/crypto"
|
||||
)
|
||||
|
||||
const (
|
||||
exampleDIDStr = "did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK"
|
||||
examplePubKeyStr = "Lm/M42cB3HkUiODQsXRcweM6TByfzEHGO9ND274JcOY="
|
||||
)
|
||||
|
||||
func TestFromPubKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
_, ecdsaP256, err := libp2p_crypto.GenerateECDSAKeyPairWithCurve(elliptic.P256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
_, ecdsaP384, err := libp2p_crypto.GenerateECDSAKeyPairWithCurve(elliptic.P384(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
_, ecdsaP521, err := libp2p_crypto.GenerateECDSAKeyPairWithCurve(elliptic.P521(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
_, ecdsaSecp256k1, err := libp2p_crypto.GenerateECDSAKeyPairWithCurve(secp256k1.S256(), rand.Reader)
|
||||
require.NoError(t, err)
|
||||
_, ed25519, err := libp2p_crypto.GenerateEd25519Key(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
_, rsa, err := libp2p_crypto.GenerateRSAKeyPair(2048, rand.Reader)
|
||||
require.NoError(t, err)
|
||||
_, secp256k1PubKey1, err := libp2p_crypto.GenerateSecp256k1Key(rand.Reader)
|
||||
require.NoError(t, err)
|
||||
|
||||
test := func(pub libp2p_crypto.PubKey, code multicodec.Code) func(t *testing.T) {
|
||||
t.Helper()
|
||||
|
||||
return func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
id, err := crypto.FromPubKey(pub)
|
||||
require.NoError(t, err)
|
||||
p, err := id.PubKey()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, pub, p)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("ECDSA with P256 curve", test(ecdsaP256, crypto.P256))
|
||||
t.Run("ECDSA with P384 curve", test(ecdsaP384, crypto.P384))
|
||||
t.Run("ECDSA with P521 curve", test(ecdsaP521, crypto.P521))
|
||||
t.Run("Ed25519", test(ed25519, crypto.Ed25519))
|
||||
t.Run("RSA", test(rsa, crypto.RSA))
|
||||
t.Run("secp256k1", test(secp256k1PubKey1, crypto.Secp256k1))
|
||||
|
||||
t.Run("ECDSA with secp256k1 curve (coerced)", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
id, err := crypto.FromPubKey(ecdsaSecp256k1)
|
||||
require.NoError(t, err)
|
||||
p, err := id.PubKey()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, pb.KeyType_Secp256k1, p.Type())
|
||||
})
|
||||
|
||||
t.Run("unmarshaled example key (secp256k1)", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
id, err := crypto.FromPubKey(examplePubKey(t))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, exampleDID(t), id)
|
||||
})
|
||||
}
|
||||
|
||||
func TestToPubKey(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
pubKey, err := crypto.ToPubKey(exampleDIDStr)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, examplePubKey(t), pubKey)
|
||||
}
|
||||
|
||||
func exampleDID(t *testing.T) crypto.DID {
|
||||
t.Helper()
|
||||
|
||||
id, err := crypto.Parse(exampleDIDStr)
|
||||
require.NoError(t, err)
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func examplePubKey(t *testing.T) libp2p_crypto.PubKey {
|
||||
t.Helper()
|
||||
|
||||
pubKeyCfg, err := libp2p_crypto.ConfigDecodeKey(examplePubKeyStr)
|
||||
require.NoError(t, err)
|
||||
|
||||
pubKey, err := libp2p_crypto.UnmarshalEd25519PublicKey(pubKeyCfg)
|
||||
require.NoError(t, err)
|
||||
|
||||
return pubKey
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
crypto "github.com/libp2p/go-libp2p/core/crypto"
|
||||
mbase "github.com/multiformats/go-multibase"
|
||||
"github.com/multiformats/go-multicodec"
|
||||
varint "github.com/multiformats/go-varint"
|
||||
)
|
||||
|
||||
// Signature algorithms from the [did:key specification]
|
||||
//
|
||||
// [did:key specification]: https://w3c-ccg.github.io/did-method-key/#signature-method-creation-algorithm
|
||||
const (
|
||||
X25519 = multicodec.X25519Pub
|
||||
Ed25519 = multicodec.Ed25519Pub // UCAN required/recommended
|
||||
P256 = multicodec.P256Pub // UCAN required
|
||||
P384 = multicodec.P384Pub
|
||||
P521 = multicodec.P521Pub
|
||||
Secp256k1 = multicodec.Secp256k1Pub // UCAN required
|
||||
RSA = multicodec.RsaPub
|
||||
)
|
||||
|
||||
// Undef can be used to represent a nil or undefined DID, using DID{}
|
||||
// directly is also acceptable.
|
||||
var Undef = DID{}
|
||||
|
||||
// DID is a Decentralized Identifier of the did:key type, directly holding a cryptographic public key.
|
||||
// [did:key format]: https://w3c-ccg.github.io/did-method-key/
|
||||
type DID struct {
|
||||
code multicodec.Code
|
||||
bytes string // as string instead of []byte to allow the == operator
|
||||
}
|
||||
|
||||
// Parse returns the DID from the string representation or an error if
|
||||
// the prefix and method are incorrect, if an unknown encryption algorithm
|
||||
// is specified or if the method-specific-identifier's bytes don't
|
||||
// represent a public key for the specified encryption algorithm.
|
||||
func Parse(str string) (DID, error) {
|
||||
const keyPrefix = "did:key:"
|
||||
|
||||
if !strings.HasPrefix(str, keyPrefix) {
|
||||
return Undef, fmt.Errorf("must start with 'did:key'")
|
||||
}
|
||||
|
||||
baseCodec, bytes, err := mbase.Decode(str[len(keyPrefix):])
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
if baseCodec != mbase.Base58BTC {
|
||||
return Undef, fmt.Errorf("not Base58BTC encoded")
|
||||
}
|
||||
code, _, err := varint.FromUvarint(bytes)
|
||||
if err != nil {
|
||||
return Undef, err
|
||||
}
|
||||
switch multicodec.Code(code) {
|
||||
case Ed25519, P256, Secp256k1, RSA:
|
||||
return DID{bytes: string(bytes), code: multicodec.Code(code)}, nil
|
||||
default:
|
||||
return Undef, fmt.Errorf("unsupported did:key multicodec: 0x%x", code)
|
||||
}
|
||||
}
|
||||
|
||||
// MustParse is like Parse but panics instead of returning an error.
|
||||
func MustParse(str string) DID {
|
||||
did, err := Parse(str)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return did
|
||||
}
|
||||
|
||||
// Defined tells if the DID is defined, not equal to Undef.
|
||||
func (d DID) Defined() bool {
|
||||
return d.code != 0 || len(d.bytes) > 0
|
||||
}
|
||||
|
||||
// PubKey returns the public key encapsulated by the did:key.
|
||||
func (d DID) PubKey() (crypto.PubKey, error) {
|
||||
unmarshaler, ok := map[multicodec.Code]crypto.PubKeyUnmarshaller{
|
||||
X25519: crypto.UnmarshalEd25519PublicKey,
|
||||
Ed25519: crypto.UnmarshalEd25519PublicKey,
|
||||
P256: ecdsaPubKeyUnmarshaler(elliptic.P256()),
|
||||
P384: ecdsaPubKeyUnmarshaler(elliptic.P384()),
|
||||
P521: ecdsaPubKeyUnmarshaler(elliptic.P521()),
|
||||
Secp256k1: crypto.UnmarshalSecp256k1PublicKey,
|
||||
RSA: rsaPubKeyUnmarshaller,
|
||||
}[d.code]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unsupported multicodec: %d", d.code)
|
||||
}
|
||||
|
||||
codeSize := varint.UvarintSize(uint64(d.code))
|
||||
return unmarshaler([]byte(d.bytes)[codeSize:])
|
||||
}
|
||||
|
||||
// String formats the decentralized identity document (DID) as a string.
|
||||
func (d DID) String() string {
|
||||
key, _ := mbase.Encode(mbase.Base58BTC, []byte(d.bytes))
|
||||
return "did:key:" + key
|
||||
}
|
||||
|
||||
func ecdsaPubKeyUnmarshaler(curve elliptic.Curve) crypto.PubKeyUnmarshaller {
|
||||
return func(data []byte) (crypto.PubKey, error) {
|
||||
x, y := elliptic.UnmarshalCompressed(curve, data)
|
||||
|
||||
ecdsaPublicKey := &ecdsa.PublicKey{
|
||||
Curve: curve,
|
||||
X: x,
|
||||
Y: y,
|
||||
}
|
||||
|
||||
pkix, err := x509.MarshalPKIXPublicKey(ecdsaPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return crypto.UnmarshalECDSAPublicKey(pkix)
|
||||
}
|
||||
}
|
||||
|
||||
func rsaPubKeyUnmarshaller(data []byte) (crypto.PubKey, error) {
|
||||
rsaPublicKey, err := x509.ParsePKCS1PublicKey(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pkix, err := x509.MarshalPKIXPublicKey(rsaPublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return crypto.UnmarshalRsaPublicKey(pkix)
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestParseDIDKey(t *testing.T) {
|
||||
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||
d, err := Parse(str)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, str, d.String())
|
||||
}
|
||||
|
||||
func TestMustParseDIDKey(t *testing.T) {
|
||||
str := "did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||
require.NotPanics(t, func() {
|
||||
d := MustParse(str)
|
||||
require.Equal(t, str, d.String())
|
||||
})
|
||||
str = "did:key:z7Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z"
|
||||
require.Panics(t, func() {
|
||||
MustParse(str)
|
||||
})
|
||||
}
|
||||
|
||||
func TestEquivalence(t *testing.T) {
|
||||
undef0 := DID{}
|
||||
undef1 := Undef
|
||||
|
||||
did0, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
|
||||
require.NoError(t, err)
|
||||
did1, err := Parse("did:key:z6Mkod5Jr3yd5SC7UDueqK4dAAw5xYJYjksy722tA9Boxc4z")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.True(t, undef0 == undef1)
|
||||
require.False(t, undef0 == did0)
|
||||
require.True(t, did0 == did1)
|
||||
require.False(t, undef1 == did1)
|
||||
}
|
||||
@@ -1,112 +0,0 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/onsonr/sonr/internal/chain/transaction"
|
||||
)
|
||||
|
||||
var (
|
||||
errNoInitHandler = errors.New("no init handler")
|
||||
errNoExecuteHandler = errors.New("account does not accept messages")
|
||||
errInvalidMessage = errors.New("invalid message")
|
||||
)
|
||||
|
||||
// NewInitBuilder creates a new InitBuilder instance.
|
||||
func NewInitBuilder() *InitBuilder {
|
||||
return &InitBuilder{}
|
||||
}
|
||||
|
||||
// InitBuilder defines a smart account's initialisation handler builder.
|
||||
type InitBuilder struct {
|
||||
// handler is the handler function that will be called when the smart account is initialized.
|
||||
// Although the function here is defined to take an any, the smart account will work
|
||||
// with a typed version of it.
|
||||
handler func(ctx context.Context, initRequest transaction.Msg) (initResponse transaction.Msg, err error)
|
||||
|
||||
// schema is the schema of the message that will be passed to the handler function.
|
||||
schema HandlerSchema
|
||||
}
|
||||
|
||||
// makeHandler returns the handler function that will be called when the smart account is initialized.
|
||||
// It returns an error if no handler was registered.
|
||||
func (i *InitBuilder) makeHandler() (func(ctx context.Context, initRequest transaction.Msg) (initResponse transaction.Msg, err error), error) {
|
||||
if i.handler == nil {
|
||||
return nil, errNoInitHandler
|
||||
}
|
||||
return i.handler, nil
|
||||
}
|
||||
|
||||
// NewExecuteBuilder creates a new ExecuteBuilder instance.
|
||||
func NewExecuteBuilder() *ExecuteBuilder {
|
||||
return &ExecuteBuilder{
|
||||
handlers: make(map[string]func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error)),
|
||||
handlersSchema: make(map[string]HandlerSchema),
|
||||
}
|
||||
}
|
||||
|
||||
// ExecuteBuilder defines a smart account's execution router, it will be used to map an execution message
|
||||
// to a handler function for a specific account.
|
||||
type ExecuteBuilder struct {
|
||||
// handlers is a map of handler functions that will be called when the smart account is executed.
|
||||
handlers map[string]func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error)
|
||||
|
||||
// handlersSchema is a map of schemas for the messages that will be passed to the handler functions
|
||||
// and the messages that will be returned by the handler functions.
|
||||
handlersSchema map[string]HandlerSchema
|
||||
|
||||
// err is the error that occurred before building the handler function.
|
||||
err error
|
||||
}
|
||||
|
||||
func (r *ExecuteBuilder) makeHandler() (func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error), error) {
|
||||
// if no handler is registered it's fine, it means the account will not be accepting execution or query messages.
|
||||
if len(r.handlers) == 0 {
|
||||
return func(ctx context.Context, _ transaction.Msg) (_ transaction.Msg, err error) {
|
||||
return nil, errNoExecuteHandler
|
||||
}, nil
|
||||
}
|
||||
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
|
||||
// build the real execution handler
|
||||
return func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error) {
|
||||
messageName := MessageName(executeRequest)
|
||||
handler, ok := r.handlers[messageName]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: no handler for message %s", errInvalidMessage, messageName)
|
||||
}
|
||||
return handler(ctx, executeRequest)
|
||||
}, nil
|
||||
}
|
||||
|
||||
// NewQueryBuilder creates a new QueryBuilder instance.
|
||||
func NewQueryBuilder() *QueryBuilder {
|
||||
return &QueryBuilder{
|
||||
er: NewExecuteBuilder(),
|
||||
}
|
||||
}
|
||||
|
||||
// QueryBuilder defines a smart account's query router, it will be used to map a query message
|
||||
// to a handler function for a specific account.
|
||||
type QueryBuilder struct {
|
||||
// er is the ExecuteBuilder, since there's no difference between the execution and query handlers API.
|
||||
er *ExecuteBuilder
|
||||
}
|
||||
|
||||
func (r *QueryBuilder) makeHandler() (func(ctx context.Context, queryRequest transaction.Msg) (queryResponse transaction.Msg, err error), error) {
|
||||
return r.er.makeHandler()
|
||||
}
|
||||
|
||||
// IsRoutingError returns true if the error is a routing error,
|
||||
// which typically occurs when a message cannot be matched to a handler.
|
||||
func IsRoutingError(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
return errors.Is(err, errInvalidMessage)
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
|
||||
"cosmossdk.io/collections"
|
||||
"cosmossdk.io/core/store"
|
||||
sdk "github.com/cosmos/cosmos-sdk/types"
|
||||
|
||||
"github.com/onsonr/sonr/internal/chain/transaction"
|
||||
"github.com/onsonr/sonr/x/did/types/internal/prefixstore"
|
||||
)
|
||||
|
||||
var AccountStatePrefix = collections.NewPrefix(255)
|
||||
|
||||
type (
|
||||
ModuleExecFunc = func(ctx context.Context, sender []byte, msg transaction.Msg) (transaction.Msg, error)
|
||||
ModuleQueryFunc = func(ctx context.Context, queryReq transaction.Msg) (transaction.Msg, error)
|
||||
)
|
||||
|
||||
type contextKey struct{}
|
||||
|
||||
type contextValue struct {
|
||||
store store.KVStore // store is the prefixed store for the account.
|
||||
sender []byte // sender is the address of the entity invoking the account action.
|
||||
whoami []byte // whoami is the address of the account being invoked.
|
||||
funds sdk.Coins // funds reports the coins sent alongside the request.
|
||||
parentContext context.Context // parentContext that was used to build the account context.
|
||||
moduleExec ModuleExecFunc // moduleExec is a function that executes a module message, when the resp type is unknown.
|
||||
moduleQuery ModuleQueryFunc // moduleQuery is a function that queries a module.
|
||||
}
|
||||
|
||||
func addCtx(ctx context.Context, value contextValue) context.Context {
|
||||
return context.WithValue(ctx, contextKey{}, value)
|
||||
}
|
||||
|
||||
func getCtx(ctx context.Context) contextValue {
|
||||
return ctx.Value(contextKey{}).(contextValue)
|
||||
}
|
||||
|
||||
// MakeAccountContext creates a new account execution context given:
|
||||
// storeSvc: which fetches the x/accounts module store.
|
||||
// accountAddr: the address of the account being invoked, which is used to give the
|
||||
// account a prefixed storage.
|
||||
// sender: the address of entity invoking the account action.
|
||||
// moduleExec: a function that executes a module message.
|
||||
// moduleQuery: a function that queries a module.
|
||||
func MakeAccountContext(
|
||||
ctx context.Context,
|
||||
storeSvc store.KVStoreService,
|
||||
accNumber uint64,
|
||||
accountAddr []byte,
|
||||
sender []byte,
|
||||
funds sdk.Coins,
|
||||
moduleExec ModuleExecFunc,
|
||||
moduleQuery ModuleQueryFunc,
|
||||
) context.Context {
|
||||
return addCtx(ctx, contextValue{
|
||||
store: makeAccountStore(ctx, storeSvc, accNumber),
|
||||
sender: sender,
|
||||
whoami: accountAddr,
|
||||
funds: funds,
|
||||
parentContext: ctx,
|
||||
moduleExec: moduleExec,
|
||||
moduleQuery: moduleQuery,
|
||||
})
|
||||
}
|
||||
|
||||
func SetSender(ctx context.Context, sender []byte) context.Context {
|
||||
v := getCtx(ctx)
|
||||
v.sender = sender
|
||||
return addCtx(v.parentContext, v)
|
||||
}
|
||||
|
||||
// makeAccountStore creates the prefixed store for the account.
|
||||
// It uses the number of the account, this gives constant size
|
||||
// bytes prefixes for the account state.
|
||||
func makeAccountStore(ctx context.Context, storeSvc store.KVStoreService, accNum uint64) store.KVStore {
|
||||
prefix := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(prefix, accNum)
|
||||
return prefixstore.New(storeSvc.OpenKVStore(ctx), append(AccountStatePrefix, prefix...))
|
||||
}
|
||||
|
||||
// ExecModule can be used to execute a message towards a module, when the response type is unknown.
|
||||
func ExecModule(ctx context.Context, msg transaction.Msg) (transaction.Msg, error) {
|
||||
// get sender
|
||||
v := getCtx(ctx)
|
||||
|
||||
resp, err := v.moduleExec(v.parentContext, v.whoami, msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// QueryModule can be used by an account to execute a module query.
|
||||
func QueryModule(ctx context.Context, req transaction.Msg) (transaction.Msg, error) {
|
||||
// we do not need to check the sender in a query because it is not a state transition.
|
||||
// we also unwrap the original context.
|
||||
v := getCtx(ctx)
|
||||
resp, err := v.moduleQuery(v.parentContext, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// openKVStore returns the prefixed store for the account given the context.
|
||||
func openKVStore(ctx context.Context) store.KVStore { return getCtx(ctx).store }
|
||||
|
||||
// Sender returns the address of the entity invoking the account action.
|
||||
func Sender(ctx context.Context) []byte {
|
||||
return getCtx(ctx).sender
|
||||
}
|
||||
|
||||
// Whoami returns the address of the account being invoked.
|
||||
func Whoami(ctx context.Context) []byte {
|
||||
return getCtx(ctx).whoami
|
||||
}
|
||||
|
||||
// Funds returns the funds associated with the execution context.
|
||||
func Funds(ctx context.Context) sdk.Coins { return getCtx(ctx).funds }
|
||||
@@ -1,66 +0,0 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
|
||||
codectypes "github.com/cosmos/cosmos-sdk/codec/types"
|
||||
"github.com/cosmos/gogoproto/proto"
|
||||
|
||||
"github.com/onsonr/sonr/internal/chain/transaction"
|
||||
)
|
||||
|
||||
// ProtoMsgG is a generic interface for protobuf messages.
|
||||
type ProtoMsgG[T any] interface {
|
||||
*T
|
||||
transaction.Msg
|
||||
}
|
||||
|
||||
type Any = codectypes.Any
|
||||
|
||||
func FindMessageByName(name string) (transaction.Msg, error) {
|
||||
typ := proto.MessageType(name)
|
||||
if typ == nil {
|
||||
return nil, fmt.Errorf("no message type found for %s", name)
|
||||
}
|
||||
return reflect.New(typ.Elem()).Interface().(transaction.Msg), nil
|
||||
}
|
||||
|
||||
func MessageName(msg transaction.Msg) string {
|
||||
return proto.MessageName(msg)
|
||||
}
|
||||
|
||||
// PackAny packs a proto message into an anypb.Any.
|
||||
func PackAny(msg transaction.Msg) (*Any, error) {
|
||||
return codectypes.NewAnyWithValue(msg)
|
||||
}
|
||||
|
||||
// UnpackAny unpacks an anypb.Any into a proto message.
|
||||
func UnpackAny[T any, PT ProtoMsgG[T]](anyPB *Any) (PT, error) {
|
||||
to := new(T)
|
||||
return to, UnpackAnyTo(anyPB, PT(to))
|
||||
}
|
||||
|
||||
func UnpackAnyTo(anyPB *Any, to transaction.Msg) error {
|
||||
return proto.Unmarshal(anyPB.Value, to)
|
||||
}
|
||||
|
||||
func UnpackAnyRaw(anyPB *Any) (proto.Message, error) {
|
||||
split := strings.Split(anyPB.TypeUrl, "/")
|
||||
name := split[len(split)-1]
|
||||
typ := proto.MessageType(name)
|
||||
if typ == nil {
|
||||
return nil, fmt.Errorf("no message type found for %s", name)
|
||||
}
|
||||
to := reflect.New(typ.Elem()).Interface().(proto.Message)
|
||||
return to, UnpackAnyTo(anyPB, to)
|
||||
}
|
||||
|
||||
func Merge(a, b transaction.Msg) {
|
||||
proto.Merge(a, b)
|
||||
}
|
||||
|
||||
func Equal(a, b transaction.Msg) bool {
|
||||
return proto.Equal(a, b)
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"cosmossdk.io/collections"
|
||||
"cosmossdk.io/core/address"
|
||||
"github.com/cosmos/cosmos-sdk/codec"
|
||||
gogoproto "github.com/cosmos/gogoproto/proto"
|
||||
|
||||
"github.com/onsonr/sonr/internal/chain/appmodule"
|
||||
"github.com/onsonr/sonr/internal/chain/transaction"
|
||||
)
|
||||
|
||||
// Dependencies are passed to the constructor of a smart account.
|
||||
type Dependencies struct {
|
||||
SchemaBuilder *collections.SchemaBuilder
|
||||
AddressCodec address.Codec
|
||||
Environment appmodule.Environment
|
||||
LegacyStateCodec interface {
|
||||
Marshal(gogoproto.Message) ([]byte, error)
|
||||
Unmarshal([]byte, gogoproto.Message) error
|
||||
}
|
||||
}
|
||||
|
||||
// AccountCreatorFunc is a function that creates an account.
|
||||
type AccountCreatorFunc = func(deps Dependencies) (string, Account, error)
|
||||
|
||||
// MakeAccountsMap creates a map of account names to account implementations
|
||||
// from a list of account creator functions.
|
||||
func MakeAccountsMap(
|
||||
cdc codec.Codec,
|
||||
addressCodec address.Codec,
|
||||
env appmodule.Environment,
|
||||
accounts []AccountCreatorFunc,
|
||||
) (map[string]Implementation, error) {
|
||||
accountsMap := make(map[string]Implementation, len(accounts))
|
||||
for _, makeAccount := range accounts {
|
||||
stateSchemaBuilder := collections.NewSchemaBuilderFromAccessor(openKVStore)
|
||||
deps := Dependencies{
|
||||
SchemaBuilder: stateSchemaBuilder,
|
||||
AddressCodec: addressCodec,
|
||||
Environment: env,
|
||||
LegacyStateCodec: cdc,
|
||||
}
|
||||
name, accountInterface, err := makeAccount(deps)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create account %s: %w", name, err)
|
||||
}
|
||||
if _, ok := accountsMap[name]; ok {
|
||||
return nil, fmt.Errorf("account %s is already registered", name)
|
||||
}
|
||||
impl, err := newImplementation(stateSchemaBuilder, accountInterface)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create implementation for account %s: %w", name, err)
|
||||
}
|
||||
accountsMap[name] = impl
|
||||
}
|
||||
|
||||
return accountsMap, nil
|
||||
}
|
||||
|
||||
// newImplementation creates a new Implementation instance given an Account implementer.
|
||||
func newImplementation(schemaBuilder *collections.SchemaBuilder, account Account) (Implementation, error) {
|
||||
// make init handler
|
||||
ir := NewInitBuilder()
|
||||
account.RegisterInitHandler(ir)
|
||||
initHandler, err := ir.makeHandler()
|
||||
if err != nil {
|
||||
return Implementation{}, err
|
||||
}
|
||||
|
||||
// make execute handler
|
||||
er := NewExecuteBuilder()
|
||||
account.RegisterExecuteHandlers(er)
|
||||
executeHandler, err := er.makeHandler()
|
||||
if err != nil {
|
||||
return Implementation{}, err
|
||||
}
|
||||
|
||||
// make query handler
|
||||
qr := NewQueryBuilder()
|
||||
account.RegisterQueryHandlers(qr)
|
||||
queryHandler, err := qr.makeHandler()
|
||||
if err != nil {
|
||||
return Implementation{}, err
|
||||
}
|
||||
|
||||
// build schema
|
||||
schema, err := schemaBuilder.Build()
|
||||
if err != nil {
|
||||
return Implementation{}, err
|
||||
}
|
||||
return Implementation{
|
||||
Init: initHandler,
|
||||
Execute: executeHandler,
|
||||
Query: queryHandler,
|
||||
CollectionsSchema: schema,
|
||||
InitHandlerSchema: ir.schema,
|
||||
QueryHandlersSchema: qr.er.handlersSchema,
|
||||
ExecuteHandlersSchema: er.handlersSchema,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Implementation wraps an Account implementer in order to provide a concrete
|
||||
// and non-generic implementation usable by the x/accounts module.
|
||||
type Implementation struct {
|
||||
// Init defines the initialisation handler for the smart account.
|
||||
Init func(ctx context.Context, msg transaction.Msg) (resp transaction.Msg, err error)
|
||||
// Execute defines the execution handler for the smart account.
|
||||
Execute func(ctx context.Context, msg transaction.Msg) (resp transaction.Msg, err error)
|
||||
// Query defines the query handler for the smart account.
|
||||
Query func(ctx context.Context, msg transaction.Msg) (resp transaction.Msg, err error)
|
||||
// CollectionsSchema represents the state schema.
|
||||
CollectionsSchema collections.Schema
|
||||
// InitHandlerSchema represents the init handler schema.
|
||||
InitHandlerSchema HandlerSchema
|
||||
// QueryHandlersSchema is the schema of the query handlers.
|
||||
QueryHandlersSchema map[string]HandlerSchema
|
||||
// ExecuteHandlersSchema is the schema of the execute handlers.
|
||||
ExecuteHandlersSchema map[string]HandlerSchema
|
||||
}
|
||||
|
||||
// HasExec returns true if the account can execute the given msg.
|
||||
func (i Implementation) HasExec(m transaction.Msg) bool {
|
||||
_, ok := i.ExecuteHandlersSchema[MessageName(m)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// HasQuery returns true if the account can execute the given request.
|
||||
func (i Implementation) HasQuery(m transaction.Msg) bool {
|
||||
_, ok := i.QueryHandlersSchema[MessageName(m)]
|
||||
return ok
|
||||
}
|
||||
|
||||
// HasInit returns true if the account uses the provided init message.
|
||||
func (i Implementation) HasInit(m transaction.Msg) bool {
|
||||
return i.InitHandlerSchema.RequestSchema.Name == MessageName(m)
|
||||
}
|
||||
|
||||
// MessageSchema defines the schema of a message.
|
||||
// A message can also define a state schema.
|
||||
type MessageSchema struct {
|
||||
// Name identifies the message name, this must be queryable from some reflection service.
|
||||
Name string
|
||||
// New is used to create a new message instance for the schema.
|
||||
New func() transaction.Msg
|
||||
}
|
||||
|
||||
// HandlerSchema defines the schema of a handler.
|
||||
type HandlerSchema struct {
|
||||
// RequestSchema defines the schema of the request.
|
||||
RequestSchema MessageSchema
|
||||
// ResponseSchema defines the schema of the response.
|
||||
ResponseSchema MessageSchema
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
package accounts
|
||||
|
||||
// Account defines a smart account interface.
|
||||
type Account interface {
|
||||
// RegisterInitHandler allows the smart account to register an initialisation handler, using
|
||||
// the provided InitBuilder. The handler will be called when the smart account is initialized
|
||||
// (deployed).
|
||||
RegisterInitHandler(builder *InitBuilder)
|
||||
|
||||
// RegisterExecuteHandlers allows the smart account to register execution handlers.
|
||||
// The smart account might also decide to not register any execution handler.
|
||||
RegisterExecuteHandlers(builder *ExecuteBuilder)
|
||||
|
||||
// RegisterQueryHandlers allows the smart account to register query handlers. The smart account
|
||||
// might also decide to not register any query handler.
|
||||
RegisterQueryHandlers(builder *QueryBuilder)
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package accounts
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
||||
"github.com/onsonr/sonr/internal/chain/transaction"
|
||||
)
|
||||
|
||||
// RegisterInitHandler registers an initialisation handler for a smart account that uses protobuf.
|
||||
func RegisterInitHandler[
|
||||
Req any, ProtoReq ProtoMsgG[Req], Resp any, ProtoResp ProtoMsgG[Resp],
|
||||
](router *InitBuilder, handler func(ctx context.Context, req ProtoReq) (ProtoResp, error),
|
||||
) {
|
||||
reqName := MessageName(ProtoReq(new(Req)))
|
||||
|
||||
router.handler = func(ctx context.Context, initRequest transaction.Msg) (initResponse transaction.Msg, err error) {
|
||||
concrete, ok := initRequest.(ProtoReq)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: wanted %s, got %T", errInvalidMessage, reqName, initRequest)
|
||||
}
|
||||
return handler(ctx, concrete)
|
||||
}
|
||||
|
||||
router.schema = HandlerSchema{
|
||||
RequestSchema: *NewProtoMessageSchema[Req, ProtoReq](),
|
||||
ResponseSchema: *NewProtoMessageSchema[Resp, ProtoResp](),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterExecuteHandler registers an execution handler for a smart account that uses protobuf.
|
||||
func RegisterExecuteHandler[
|
||||
Req any, ProtoReq ProtoMsgG[Req], Resp any, ProtoResp ProtoMsgG[Resp],
|
||||
](router *ExecuteBuilder, handler func(ctx context.Context, req ProtoReq) (ProtoResp, error),
|
||||
) {
|
||||
reqName := MessageName(ProtoReq(new(Req)))
|
||||
// check if not registered already
|
||||
if _, ok := router.handlers[reqName]; ok {
|
||||
router.err = fmt.Errorf("handler already registered for message %s", reqName)
|
||||
return
|
||||
}
|
||||
|
||||
router.handlers[reqName] = func(ctx context.Context, executeRequest transaction.Msg) (executeResponse transaction.Msg, err error) {
|
||||
concrete, ok := executeRequest.(ProtoReq)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: wanted %s, got %T", errInvalidMessage, reqName, executeRequest)
|
||||
}
|
||||
return handler(ctx, concrete)
|
||||
}
|
||||
|
||||
router.handlersSchema[reqName] = HandlerSchema{
|
||||
RequestSchema: *NewProtoMessageSchema[Req, ProtoReq](),
|
||||
ResponseSchema: *NewProtoMessageSchema[Resp, ProtoResp](),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterQueryHandler registers a query handler for a smart account that uses protobuf.
|
||||
func RegisterQueryHandler[
|
||||
Req any, ProtoReq ProtoMsgG[Req], Resp any, ProtoResp ProtoMsgG[Resp],
|
||||
](router *QueryBuilder, handler func(ctx context.Context, req ProtoReq) (ProtoResp, error),
|
||||
) {
|
||||
RegisterExecuteHandler(router.er, handler)
|
||||
}
|
||||
|
||||
func NewProtoMessageSchema[T any, PT ProtoMsgG[T]]() *MessageSchema {
|
||||
msg := PT(new(T))
|
||||
if _, ok := (interface{}(msg)).(proto.Message); ok {
|
||||
panic("protov2 messages are not supported")
|
||||
}
|
||||
return &MessageSchema{
|
||||
Name: MessageName(msg),
|
||||
New: func() transaction.Msg {
|
||||
return PT(new(T))
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,223 +0,0 @@
|
||||
// Package prefixstore provides a store that prefixes all keys with a given
|
||||
// prefix. It is used to isolate storage reads and writes for an account.
|
||||
// Implementation taken from cosmossdk.io/store/prefix, and adapted to
|
||||
// the cosmossdk.io/core/store.KVStore interface.
|
||||
package prefixstore
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
|
||||
"cosmossdk.io/core/store"
|
||||
)
|
||||
|
||||
// New creates a new prefix store using the provided bytes prefix.
|
||||
func New(store store.KVStore, prefix []byte) store.KVStore {
|
||||
return Store{
|
||||
parent: store,
|
||||
prefix: prefix,
|
||||
}
|
||||
}
|
||||
|
||||
var _ store.KVStore = Store{}
|
||||
|
||||
// Store is similar with cometbft/cometbft-db/blob/v1.0.1/prefixdb.go
|
||||
// both gives access only to the limited subset of the store
|
||||
// for convenience or safety
|
||||
type Store struct {
|
||||
parent store.KVStore
|
||||
prefix []byte
|
||||
}
|
||||
|
||||
func cloneAppend(bz, tail []byte) (res []byte) {
|
||||
res = make([]byte, len(bz)+len(tail))
|
||||
copy(res, bz)
|
||||
copy(res[len(bz):], tail)
|
||||
return
|
||||
}
|
||||
|
||||
func (s Store) key(key []byte) (res []byte) {
|
||||
if key == nil {
|
||||
panic("nil key on Store")
|
||||
}
|
||||
res = cloneAppend(s.prefix, key)
|
||||
return
|
||||
}
|
||||
|
||||
// Implements KVStore
|
||||
func (s Store) Get(key []byte) ([]byte, error) {
|
||||
return s.parent.Get(s.key(key))
|
||||
}
|
||||
|
||||
// Implements KVStore
|
||||
func (s Store) Has(key []byte) (bool, error) {
|
||||
return s.parent.Has(s.key(key))
|
||||
}
|
||||
|
||||
// Implements KVStore
|
||||
func (s Store) Set(key, value []byte) error {
|
||||
return s.parent.Set(s.key(key), value)
|
||||
}
|
||||
|
||||
// Implements KVStore
|
||||
func (s Store) Delete(key []byte) error { return s.parent.Delete(s.key(key)) }
|
||||
|
||||
// Implements KVStore
|
||||
// Check https://github.com/cometbft/cometbft-db/blob/v1.0.1/prefixdb.go#L109
|
||||
func (s Store) Iterator(start, end []byte) (store.Iterator, error) {
|
||||
newstart := cloneAppend(s.prefix, start)
|
||||
|
||||
var newend []byte
|
||||
if end == nil {
|
||||
newend = cpIncr(s.prefix)
|
||||
} else {
|
||||
newend = cloneAppend(s.prefix, end)
|
||||
}
|
||||
|
||||
iter, err := s.parent.Iterator(newstart, newend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newPrefixIterator(s.prefix, start, end, iter), nil
|
||||
}
|
||||
|
||||
// ReverseIterator implements KVStore
|
||||
// Check https://github.com/cometbft/cometbft-db/blob/v1.0.1/prefixdb.go#L132
|
||||
func (s Store) ReverseIterator(start, end []byte) (store.Iterator, error) {
|
||||
newstart := cloneAppend(s.prefix, start)
|
||||
|
||||
var newend []byte
|
||||
if end == nil {
|
||||
newend = cpIncr(s.prefix)
|
||||
} else {
|
||||
newend = cloneAppend(s.prefix, end)
|
||||
}
|
||||
|
||||
iter, err := s.parent.ReverseIterator(newstart, newend)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newPrefixIterator(s.prefix, start, end, iter), nil
|
||||
}
|
||||
|
||||
var _ store.Iterator = (*prefixIterator)(nil)
|
||||
|
||||
type prefixIterator struct {
|
||||
prefix []byte
|
||||
start []byte
|
||||
end []byte
|
||||
iter store.Iterator
|
||||
valid bool
|
||||
}
|
||||
|
||||
func newPrefixIterator(prefix, start, end []byte, parent store.Iterator) *prefixIterator {
|
||||
return &prefixIterator{
|
||||
prefix: prefix,
|
||||
start: start,
|
||||
end: end,
|
||||
iter: parent,
|
||||
valid: parent.Valid() && bytes.HasPrefix(parent.Key(), prefix),
|
||||
}
|
||||
}
|
||||
|
||||
// Implements Iterator
|
||||
func (pi *prefixIterator) Domain() ([]byte, []byte) {
|
||||
return pi.start, pi.end
|
||||
}
|
||||
|
||||
// Implements Iterator
|
||||
func (pi *prefixIterator) Valid() bool {
|
||||
return pi.valid && pi.iter.Valid()
|
||||
}
|
||||
|
||||
// Implements Iterator
|
||||
func (pi *prefixIterator) Next() {
|
||||
if !pi.valid {
|
||||
panic("prefixIterator invalid, cannot call Next()")
|
||||
}
|
||||
|
||||
if pi.iter.Next(); !pi.iter.Valid() || !bytes.HasPrefix(pi.iter.Key(), pi.prefix) {
|
||||
// TODO: shouldn't pi be set to nil instead?
|
||||
pi.valid = false
|
||||
}
|
||||
}
|
||||
|
||||
// Implements Iterator
|
||||
func (pi *prefixIterator) Key() (key []byte) {
|
||||
if !pi.valid {
|
||||
panic("prefixIterator invalid, cannot call Key()")
|
||||
}
|
||||
|
||||
key = pi.iter.Key()
|
||||
key = stripPrefix(key, pi.prefix)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Implements Iterator
|
||||
func (pi *prefixIterator) Value() []byte {
|
||||
if !pi.valid {
|
||||
panic("prefixIterator invalid, cannot call Value()")
|
||||
}
|
||||
|
||||
return pi.iter.Value()
|
||||
}
|
||||
|
||||
// Implements Iterator
|
||||
func (pi *prefixIterator) Close() error {
|
||||
return pi.iter.Close()
|
||||
}
|
||||
|
||||
// Error returns an error if the prefixIterator is invalid defined by the Valid
|
||||
// method.
|
||||
func (pi *prefixIterator) Error() error {
|
||||
if !pi.Valid() {
|
||||
return errors.New("invalid prefixIterator")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// copied from github.com/cometbft/cometbft-db/blob/v1.0.1/prefixdb.go
|
||||
func stripPrefix(key, prefix []byte) []byte {
|
||||
if len(key) < len(prefix) || !bytes.Equal(key[:len(prefix)], prefix) {
|
||||
panic("should not happen")
|
||||
}
|
||||
|
||||
return key[len(prefix):]
|
||||
}
|
||||
|
||||
// wrapping types.PrefixEndBytes
|
||||
func cpIncr(bz []byte) []byte {
|
||||
return prefixEndBytes(bz)
|
||||
}
|
||||
|
||||
// prefixEndBytes returns the []byte that would end a
|
||||
// range query for all []byte with a certain prefix
|
||||
// Deals with last byte of prefix being FF without overflowing
|
||||
func prefixEndBytes(prefix []byte) []byte {
|
||||
if len(prefix) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
end := make([]byte, len(prefix))
|
||||
copy(end, prefix)
|
||||
|
||||
for {
|
||||
if end[len(end)-1] != byte(255) {
|
||||
end[len(end)-1]++
|
||||
break
|
||||
}
|
||||
|
||||
end = end[:len(end)-1]
|
||||
|
||||
if len(end) == 0 {
|
||||
end = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return end
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package embed
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ipfs/boxo/files"
|
||||
"github.com/onsonr/sonr/internal/config/motr"
|
||||
)
|
||||
|
||||
const SchemaVersion = 1
|
||||
const (
|
||||
AppManifestFileName = "app.webmanifest"
|
||||
DWNConfigFileName = "dwn.json"
|
||||
IndexHTMLFileName = "index.html"
|
||||
MainJSFileName = "main.js"
|
||||
ServiceWorkerFileName = "sw.js"
|
||||
)
|
||||
|
||||
// spawnVaultDirectory creates a new directory with the default files
|
||||
func NewVaultFS(cfg *motr.Config) (files.Directory, error) {
|
||||
manifestBz, err := NewWebManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cnfBz, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return files.NewMapDirectory(map[string]files.Node{
|
||||
AppManifestFileName: files.NewBytesFile(manifestBz),
|
||||
DWNConfigFileName: files.NewBytesFile(cnfBz),
|
||||
IndexHTMLFileName: files.NewBytesFile(IndexHTML),
|
||||
MainJSFileName: files.NewBytesFile(MainJS),
|
||||
ServiceWorkerFileName: files.NewBytesFile(WorkerJS),
|
||||
}), nil
|
||||
}
|
||||
|
||||
// NewVaultConfig returns the default vault config
|
||||
func NewVaultConfig(addr string, ucanCID string) *motr.Config {
|
||||
return &motr.Config{
|
||||
MotrToken: ucanCID,
|
||||
MotrAddress: addr,
|
||||
IpfsGatewayUrl: "http://localhost:80",
|
||||
SonrApiUrl: "http://localhost:1317",
|
||||
SonrRpcUrl: "http://localhost:26657",
|
||||
SonrChainId: "sonr-testnet-1",
|
||||
}
|
||||
}
|
||||
@@ -1,138 +0,0 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sonr DWN</title>
|
||||
|
||||
<!-- HTMX -->
|
||||
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
||||
|
||||
<!-- WASM Support -->
|
||||
<script src="https://cdn.jsdelivr.net/gh/golang/go@go1.22.5/misc/wasm/wasm_exec.js"></script>
|
||||
|
||||
<!-- Main JS -->
|
||||
<script src="main.js"></script>
|
||||
|
||||
<!-- Tailwind (assuming you're using it based on your classes) -->
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
|
||||
<!-- Add manifest for PWA support -->
|
||||
<link
|
||||
rel="manifest"
|
||||
href="/app.webmanifest"
|
||||
crossorigin="use-credentials"
|
||||
/>
|
||||
|
||||
<!-- Offline detection styles -->
|
||||
<style>
|
||||
.offline-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.offline .offline-indicator {
|
||||
display: block;
|
||||
background: #f44336;
|
||||
color: white;
|
||||
text-align: center;
|
||||
padding: 0.5rem;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body
|
||||
class="flex items-center justify-center h-full bg-zinc-50 lg:p-24 md:16 p-4"
|
||||
>
|
||||
<!-- Offline indicator -->
|
||||
<div class="offline-indicator">
|
||||
You are currently offline. Some features may be limited.
|
||||
</div>
|
||||
|
||||
<!-- Loading indicator -->
|
||||
<div
|
||||
id="loading-indicator"
|
||||
class="fixed top-0 left-0 w-full h-1 bg-blue-200 transition-all duration-300"
|
||||
style="display: none"
|
||||
>
|
||||
<div class="h-full bg-blue-600 w-0 transition-all duration-300"></div>
|
||||
</div>
|
||||
|
||||
<main
|
||||
class="flex-row items-center justify-center mx-auto w-fit max-w-screen-sm gap-y-3"
|
||||
>
|
||||
<div
|
||||
id="content"
|
||||
hx-get="/#"
|
||||
hx-trigger="load"
|
||||
hx-swap="outerHTML"
|
||||
hx-indicator="#loading-indicator"
|
||||
>
|
||||
Loading...
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<!-- WASM Ready Indicator (hidden) -->
|
||||
<div
|
||||
id="wasm-status"
|
||||
class="hidden fixed bottom-4 right-4 p-2 rounded-md bg-green-500 text-white"
|
||||
hx-swap-oob="true"
|
||||
>
|
||||
WASM Ready
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Initialize service worker
|
||||
if ("serviceWorker" in navigator) {
|
||||
window.addEventListener("load", async function () {
|
||||
try {
|
||||
const registration =
|
||||
await navigator.serviceWorker.register("/sw.js");
|
||||
console.log(
|
||||
"Service Worker registered with scope:",
|
||||
registration.scope,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Service Worker registration failed:", error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// HTMX loading indicator
|
||||
htmx.on("htmx:beforeRequest", function (evt) {
|
||||
document.getElementById("loading-indicator").style.display = "block";
|
||||
});
|
||||
|
||||
htmx.on("htmx:afterRequest", function (evt) {
|
||||
document.getElementById("loading-indicator").style.display = "none";
|
||||
});
|
||||
|
||||
// WASM ready event handler
|
||||
document.addEventListener("wasm-ready", function () {
|
||||
const status = document.getElementById("wasm-status");
|
||||
status.classList.remove("hidden");
|
||||
setTimeout(() => {
|
||||
status.classList.add("hidden");
|
||||
}, 3000);
|
||||
});
|
||||
|
||||
// Offline status handler
|
||||
window.addEventListener("offline", function () {
|
||||
document.body.classList.add("offline");
|
||||
});
|
||||
|
||||
window.addEventListener("online", function () {
|
||||
document.body.classList.remove("offline");
|
||||
});
|
||||
|
||||
// Initial offline check
|
||||
if (!navigator.onLine) {
|
||||
document.body.classList.add("offline");
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,152 +0,0 @@
|
||||
// MessageChannel for WASM communication
|
||||
let wasmChannel;
|
||||
let wasmPort;
|
||||
|
||||
async function initWasmChannel() {
|
||||
wasmChannel = new MessageChannel();
|
||||
wasmPort = wasmChannel.port1;
|
||||
|
||||
// Setup message handling from WASM
|
||||
wasmPort.onmessage = (event) => {
|
||||
const { type, data } = event.data;
|
||||
switch (type) {
|
||||
case 'WASM_READY':
|
||||
console.log('WASM is ready');
|
||||
document.dispatchEvent(new CustomEvent('wasm-ready'));
|
||||
break;
|
||||
case 'RESPONSE':
|
||||
handleWasmResponse(data);
|
||||
break;
|
||||
case 'SYNC_COMPLETE':
|
||||
handleSyncComplete(data);
|
||||
break;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Initialize WebAssembly and Service Worker
|
||||
async function init() {
|
||||
try {
|
||||
// Register service worker
|
||||
if ('serviceWorker' in navigator) {
|
||||
const registration = await navigator.serviceWorker.register('./sw.js');
|
||||
console.log('ServiceWorker registered');
|
||||
|
||||
// Wait for the service worker to be ready
|
||||
await navigator.serviceWorker.ready;
|
||||
|
||||
// Initialize MessageChannel
|
||||
await initWasmChannel();
|
||||
|
||||
// Send the MessageChannel port to the service worker
|
||||
navigator.serviceWorker.controller.postMessage({
|
||||
type: 'PORT_INITIALIZATION',
|
||||
port: wasmChannel.port2
|
||||
}, [wasmChannel.port2]);
|
||||
|
||||
// Register for periodic sync if available
|
||||
if ('periodicSync' in registration) {
|
||||
try {
|
||||
await registration.periodicSync.register('wasm-sync', {
|
||||
minInterval: 24 * 60 * 60 * 1000 // 24 hours
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('Periodic sync could not be registered:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize HTMX with custom config
|
||||
htmx.config.withCredentials = true;
|
||||
htmx.config.wsReconnectDelay = 'full-jitter';
|
||||
|
||||
// Override HTMX's internal request handling
|
||||
htmx.config.beforeRequest = function (config) {
|
||||
// Add request ID for tracking
|
||||
const requestId = 'req_' + Date.now();
|
||||
config.headers['X-Wasm-Request-ID'] = requestId;
|
||||
|
||||
// If offline, handle through service worker
|
||||
if (!navigator.onLine) {
|
||||
return false; // Let service worker handle it
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Handle HTMX after request
|
||||
htmx.config.afterRequest = function (config) {
|
||||
// Additional processing after request if needed
|
||||
};
|
||||
|
||||
// Handle HTMX errors
|
||||
htmx.config.errorHandler = function (error) {
|
||||
console.error('HTMX Error:', error);
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
console.error('Initialization failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function handleWasmResponse(data) {
|
||||
const { requestId, response } = data;
|
||||
// Process the WASM response
|
||||
// This might update the UI or trigger HTMX swaps
|
||||
const targetElement = document.querySelector(`[data-request-id="${requestId}"]`);
|
||||
if (targetElement) {
|
||||
htmx.process(targetElement);
|
||||
}
|
||||
}
|
||||
|
||||
function handleSyncComplete(data) {
|
||||
const { url } = data;
|
||||
// Handle successful sync
|
||||
// Maybe refresh the relevant part of the UI
|
||||
htmx.trigger('body', 'sync:complete', { url });
|
||||
}
|
||||
|
||||
// Handle offline status changes
|
||||
window.addEventListener('online', () => {
|
||||
document.body.classList.remove('offline');
|
||||
// Trigger sync when back online
|
||||
if (wasmPort) {
|
||||
wasmPort.postMessage({ type: 'SYNC_REQUEST' });
|
||||
}
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
document.body.classList.add('offline');
|
||||
});
|
||||
|
||||
// Custom event handlers for HTMX
|
||||
document.addEventListener('htmx:beforeRequest', (event) => {
|
||||
const { elt, xhr } = event.detail;
|
||||
// Add request tracking
|
||||
const requestId = xhr.headers['X-Wasm-Request-ID'];
|
||||
elt.setAttribute('data-request-id', requestId);
|
||||
});
|
||||
|
||||
document.addEventListener('htmx:afterRequest', (event) => {
|
||||
const { elt, successful } = event.detail;
|
||||
if (successful) {
|
||||
elt.removeAttribute('data-request-id');
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize everything when the page loads
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
// Export functions that might be needed by WASM
|
||||
window.wasmBridge = {
|
||||
triggerUIUpdate: function (selector, content) {
|
||||
const target = document.querySelector(selector);
|
||||
if (target) {
|
||||
htmx.process(htmx.parse(content).forEach(node => target.appendChild(node)));
|
||||
}
|
||||
},
|
||||
|
||||
showNotification: function (message, type = 'info') {
|
||||
// Implement notification system
|
||||
console.log(`${type}: ${message}`);
|
||||
}
|
||||
};
|
||||
@@ -1,258 +0,0 @@
|
||||
// Cache names for different types of resources
|
||||
const CACHE_NAMES = {
|
||||
wasm: 'wasm-cache-v1',
|
||||
static: 'static-cache-v1',
|
||||
dynamic: 'dynamic-cache-v1'
|
||||
};
|
||||
|
||||
// Import required scripts
|
||||
importScripts(
|
||||
"https://cdn.jsdelivr.net/gh/golang/go@go1.22.5/misc/wasm/wasm_exec.js",
|
||||
"https://cdn.jsdelivr.net/gh/nlepage/go-wasm-http-server@v1.1.0/sw.js",
|
||||
);
|
||||
|
||||
// Initialize WASM HTTP listener
|
||||
const wasmInstance = registerWasmHTTPListener("https://cdn.sonr.id/wasm/app.wasm");
|
||||
|
||||
// MessageChannel port for WASM communication
|
||||
let wasmPort;
|
||||
|
||||
// Request queue for offline operations
|
||||
let requestQueue = new Map();
|
||||
|
||||
// Setup message channel handler
|
||||
self.addEventListener('message', async (event) => {
|
||||
if (event.data.type === 'PORT_INITIALIZATION') {
|
||||
wasmPort = event.data.port;
|
||||
setupWasmCommunication();
|
||||
}
|
||||
});
|
||||
|
||||
function setupWasmCommunication() {
|
||||
wasmPort.onmessage = async (event) => {
|
||||
const { type, data } = event.data;
|
||||
|
||||
switch (type) {
|
||||
case 'WASM_REQUEST':
|
||||
handleWasmRequest(data);
|
||||
break;
|
||||
case 'SYNC_REQUEST':
|
||||
processSyncQueue();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// Notify that WASM is ready
|
||||
wasmPort.postMessage({ type: 'WASM_READY' });
|
||||
}
|
||||
|
||||
// Enhanced install event
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
Promise.all([
|
||||
skipWaiting(),
|
||||
// Cache WASM binary and essential resources
|
||||
caches.open(CACHE_NAMES.wasm).then(cache =>
|
||||
cache.addAll([
|
||||
'https://cdn.sonr.id/wasm/app.wasm',
|
||||
'https://cdn.jsdelivr.net/gh/golang/go@go1.22.5/misc/wasm/wasm_exec.js'
|
||||
])
|
||||
)
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
// Enhanced activate event
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
Promise.all([
|
||||
clients.claim(),
|
||||
// Clean up old caches
|
||||
caches.keys().then(keys =>
|
||||
Promise.all(
|
||||
keys.map(key => {
|
||||
if (!Object.values(CACHE_NAMES).includes(key)) {
|
||||
return caches.delete(key);
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
// Intercept fetch events
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const request = event.request;
|
||||
|
||||
// Handle API requests differently from static resources
|
||||
if (request.url.includes('/api/')) {
|
||||
event.respondWith(handleApiRequest(request));
|
||||
} else {
|
||||
event.respondWith(handleStaticRequest(request));
|
||||
}
|
||||
});
|
||||
|
||||
async function handleApiRequest(request) {
|
||||
try {
|
||||
// Try to make the request
|
||||
const response = await fetch(request.clone());
|
||||
|
||||
// If successful, pass through WASM handler
|
||||
if (response.ok) {
|
||||
return await processWasmResponse(request, response);
|
||||
}
|
||||
|
||||
// If offline or failed, queue the request
|
||||
await queueRequest(request);
|
||||
|
||||
// Return cached response if available
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
// Return offline response
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Currently offline' }),
|
||||
{
|
||||
status: 503,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
} catch (error) {
|
||||
await queueRequest(request);
|
||||
return new Response(
|
||||
JSON.stringify({ error: 'Request failed' }),
|
||||
{
|
||||
status: 500,
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStaticRequest(request) {
|
||||
// Check cache first
|
||||
const cachedResponse = await caches.match(request);
|
||||
if (cachedResponse) {
|
||||
return cachedResponse;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(request);
|
||||
|
||||
// Cache successful responses
|
||||
if (response.ok) {
|
||||
const cache = await caches.open(CACHE_NAMES.static);
|
||||
cache.put(request, response.clone());
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
// Return offline page for navigation requests
|
||||
if (request.mode === 'navigate') {
|
||||
return caches.match('/offline.html');
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
async function processWasmResponse(request, response) {
|
||||
// Clone the response before processing
|
||||
const responseClone = response.clone();
|
||||
|
||||
try {
|
||||
// Process through WASM
|
||||
const processedResponse = await wasmInstance.processResponse(responseClone);
|
||||
|
||||
// Notify client through message channel
|
||||
if (wasmPort) {
|
||||
wasmPort.postMessage({
|
||||
type: 'RESPONSE',
|
||||
requestId: request.headers.get('X-Wasm-Request-ID'),
|
||||
response: processedResponse
|
||||
});
|
||||
}
|
||||
|
||||
return processedResponse;
|
||||
} catch (error) {
|
||||
console.error('WASM processing error:', error);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
async function queueRequest(request) {
|
||||
const serializedRequest = await serializeRequest(request);
|
||||
requestQueue.set(request.url, serializedRequest);
|
||||
|
||||
// Register for background sync
|
||||
try {
|
||||
await self.registration.sync.register('wasm-sync');
|
||||
} catch (error) {
|
||||
console.error('Sync registration failed:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async function serializeRequest(request) {
|
||||
const headers = {};
|
||||
for (const [key, value] of request.headers.entries()) {
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
return {
|
||||
url: request.url,
|
||||
method: request.method,
|
||||
headers,
|
||||
body: await request.text(),
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
// Handle background sync
|
||||
self.addEventListener('sync', (event) => {
|
||||
if (event.tag === 'wasm-sync') {
|
||||
event.waitUntil(processSyncQueue());
|
||||
}
|
||||
});
|
||||
|
||||
async function processSyncQueue() {
|
||||
const requests = Array.from(requestQueue.values());
|
||||
|
||||
for (const serializedRequest of requests) {
|
||||
try {
|
||||
const response = await fetch(new Request(serializedRequest.url, {
|
||||
method: serializedRequest.method,
|
||||
headers: serializedRequest.headers,
|
||||
body: serializedRequest.body
|
||||
}));
|
||||
|
||||
if (response.ok) {
|
||||
requestQueue.delete(serializedRequest.url);
|
||||
|
||||
// Notify client of successful sync
|
||||
if (wasmPort) {
|
||||
wasmPort.postMessage({
|
||||
type: 'SYNC_COMPLETE',
|
||||
url: serializedRequest.url
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Sync failed for request:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle payment requests
|
||||
self.addEventListener("canmakepayment", function (e) {
|
||||
e.respondWith(Promise.resolve(true));
|
||||
});
|
||||
|
||||
// Handle periodic sync if available
|
||||
self.addEventListener('periodicsync', (event) => {
|
||||
if (event.tag === 'wasm-sync') {
|
||||
event.waitUntil(processSyncQueue());
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
package embed
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
|
||||
//go:embed index.html
|
||||
var IndexHTML []byte
|
||||
|
||||
//go:embed main.js
|
||||
var MainJS []byte
|
||||
|
||||
//go:embed sw.js
|
||||
var WorkerJS []byte
|
||||
|
||||
func getSchema(structType interface{}) string {
|
||||
t := reflect.TypeOf(structType)
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
if t.Kind() != reflect.Struct {
|
||||
return ""
|
||||
}
|
||||
|
||||
var fields []string
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
fieldName := toCamelCase(field.Name)
|
||||
fields = append(fields, fieldName)
|
||||
}
|
||||
|
||||
// Add "++" at the beginning, separated by a comma
|
||||
return "++, " + strings.Join(fields, ", ")
|
||||
}
|
||||
|
||||
func toCamelCase(s string) string {
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
if len(s) == 1 {
|
||||
return strings.ToLower(s)
|
||||
}
|
||||
return strings.ToLower(s[:1]) + s[1:]
|
||||
}
|
||||
@@ -1,124 +0,0 @@
|
||||
package embed
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
func NewWebManifest() ([]byte, error) {
|
||||
return json.Marshal(baseWebManifest)
|
||||
}
|
||||
|
||||
var baseWebManifest = WebManifest{
|
||||
Name: "Sonr Vault",
|
||||
ShortName: "Sonr.ID",
|
||||
StartURL: "/index.html",
|
||||
Display: "standalone",
|
||||
DisplayOverride: []string{
|
||||
"fullscreen",
|
||||
"minimal-ui",
|
||||
},
|
||||
Icons: []IconDefinition{
|
||||
{
|
||||
Src: "/icons/icon-192x192.png",
|
||||
Sizes: "192x192",
|
||||
Type: "image/png",
|
||||
},
|
||||
},
|
||||
ServiceWorker: ServiceWorker{
|
||||
Scope: "/",
|
||||
Src: "/sw.js",
|
||||
UseCache: true,
|
||||
},
|
||||
ProtocolHandlers: []ProtocolHandler{
|
||||
{
|
||||
Scheme: "did.sonr",
|
||||
URL: "/resolve/sonr/%s",
|
||||
},
|
||||
{
|
||||
Scheme: "did.eth",
|
||||
URL: "/resolve/eth/%s",
|
||||
},
|
||||
{
|
||||
Scheme: "did.btc",
|
||||
URL: "/resolve/btc/%s",
|
||||
},
|
||||
{
|
||||
Scheme: "did.usdc",
|
||||
URL: "/resolve/usdc/%s",
|
||||
},
|
||||
{
|
||||
Scheme: "did.ipfs",
|
||||
URL: "/resolve/ipfs/%s",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type WebManifest struct {
|
||||
// Required fields
|
||||
Name string `json:"name"` // Full name of the application
|
||||
ShortName string `json:"short_name"` // Short version of the name
|
||||
|
||||
// Display and appearance
|
||||
Description string `json:"description,omitempty"` // Purpose and features of the application
|
||||
Display string `json:"display,omitempty"` // Preferred display mode: fullscreen, standalone, minimal-ui, browser
|
||||
DisplayOverride []string `json:"display_override,omitempty"`
|
||||
ThemeColor string `json:"theme_color,omitempty"` // Default theme color for the application
|
||||
BackgroundColor string `json:"background_color,omitempty"` // Background color during launch
|
||||
Orientation string `json:"orientation,omitempty"` // Default orientation: any, natural, landscape, portrait
|
||||
|
||||
// URLs and scope
|
||||
StartURL string `json:"start_url"` // Starting URL when launching
|
||||
Scope string `json:"scope,omitempty"` // Navigation scope of the web application
|
||||
ServiceWorker ServiceWorker `json:"service_worker,omitempty"`
|
||||
|
||||
// Icons
|
||||
Icons []IconDefinition `json:"icons,omitempty"`
|
||||
|
||||
// Optional features
|
||||
RelatedApplications []RelatedApplication `json:"related_applications,omitempty"`
|
||||
PreferRelatedApplications bool `json:"prefer_related_applications,omitempty"`
|
||||
Shortcuts []Shortcut `json:"shortcuts,omitempty"`
|
||||
|
||||
// Experimental features (uncomment if needed)
|
||||
FileHandlers []FileHandler `json:"file_handlers,omitempty"`
|
||||
ProtocolHandlers []ProtocolHandler `json:"protocol_handlers,omitempty"`
|
||||
}
|
||||
|
||||
type FileHandler struct {
|
||||
Action string `json:"action"`
|
||||
Accept map[string][]string `json:"accept"`
|
||||
}
|
||||
|
||||
type LaunchHandler struct {
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
type IconDefinition struct {
|
||||
Src string `json:"src"`
|
||||
Sizes string `json:"sizes"`
|
||||
Type string `json:"type,omitempty"`
|
||||
Purpose string `json:"purpose,omitempty"`
|
||||
}
|
||||
|
||||
type ProtocolHandler struct {
|
||||
Scheme string `json:"scheme"`
|
||||
URL string `json:"url"`
|
||||
}
|
||||
|
||||
type RelatedApplication struct {
|
||||
Platform string `json:"platform"`
|
||||
URL string `json:"url,omitempty"`
|
||||
ID string `json:"id,omitempty"`
|
||||
}
|
||||
|
||||
type Shortcut struct {
|
||||
Name string `json:"name"`
|
||||
ShortName string `json:"short_name,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
URL string `json:"url"`
|
||||
Icons []IconDefinition `json:"icons,omitempty"`
|
||||
}
|
||||
|
||||
type ServiceWorker struct {
|
||||
Scope string `json:"scope"`
|
||||
Src string `json:"src"`
|
||||
UseCache bool `json:"use_cache"`
|
||||
}
|
||||
@@ -32,13 +32,11 @@ func (k Querier) Params(c context.Context, req *types.QueryParamsRequest) (*type
|
||||
// OriginExists implements types.QueryServer.
|
||||
func (k Querier) OriginExists(goCtx context.Context, req *types.QueryOriginExistsRequest) (*types.QueryOriginExistsResponse, error) {
|
||||
// ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
panic("OriginExists is unimplemented")
|
||||
return &types.QueryOriginExistsResponse{}, nil
|
||||
}
|
||||
|
||||
// ResolveOrigin implements types.QueryServer.
|
||||
func (k Querier) ResolveOrigin(goCtx context.Context, req *types.QueryResolveOriginRequest) (*types.QueryResolveOriginResponse, error) {
|
||||
// ctx := sdk.UnwrapSDKContext(goCtx)
|
||||
panic("ResolveOrigin is unimplemented")
|
||||
return &types.QueryResolveOriginResponse{}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user