Skip to content

How to setup a xlm.sh site

xlm.sh is a gateway for .xlm websites, providing a seamless experience for accessing decentralized content. It simplifies the process of hosting and visiting dApps and static sites built on IPFS/IPNS, making it easier for users to interact with the decentralized ecosystem.

To set up a xlm.sh site, only a few steps are required:

We will guide you through the process of setting up a xlm.sh site in the following sections.

Get a Stellar Soroban Domain

To start setting up your xlm.sh site, you first need to obtain a Soroban Domain. Here's how:

  • Visit the official Soroban Domains website at https://app.sorobandomains.org/
  • Search and select your desired domain name
  • Set up the domain address:
    • Point your domain to an address you control
    • The simplest way is to click "Use owner address" to automatically use your current wallet address
  • Complete the registration process

This domain will serve as your personal identifier in the Soroban ecosystem, replacing the traditional long string of characters with a more user-friendly name. Here we have got a Stellar Soroban Domain: example.xlm.

get-a-soroban-domain-image

Upload your content to IPFS

To upload content to IPFS, you can use either IPFS Desktop or the command line tools. However, for beginners, using an IPFS Pinning Service might be a simpler option. There are several providers offering these services, including: web3.storage, Pinata and Filebase, among others.

Let's take web3.storage as an example:

  • Create an account to get free quota
  • Use their simple web interface to upload your website files directly - no command line required
  • Once uploaded, you'll receive a CID (Content Identifier)
  • This CID is your website's unique address on IPFS

For example, after uploading our site content we received the CID: bafybeifq2rzpqnqrsdupncmkmhs3ckxxjhuvdcbvydkgvch3ms24k5lo7q.

upload-to-ipfs

Set up IPFS content for your Stellar account

Now that we have both a Stellar Soroban Domain and an IPFS CID, we need to link them together so that your website can be accessed through your_domain.xlm.sh.

Below is a form where you only need to:

  • Enter your domain name
  • Enter your IPFS CID (or IPNS name)
  • Click the confirm button

We will then build a transaction for you and guide you to Stellar Laboratory to complete the signing and submission process.

Type
Soroban Domain
IPFS CID

Set up IPFS content for contract address

If you have pointed your Soroban Domain to a contract address (an address starting with C), then you need to use code to set the IPFS record. Here is an example code snippet:

python
from enum import Enum
from typing import Optional, Union

from Crypto.Hash import keccak
from stellar_sdk import scval, xdr, MuxedAccount, Keypair, Network
from stellar_sdk.contract import AssembledTransaction, ContractClient

IPFS_KEY = "ipfs"
IPNS_KEY = "ipns"


class ValueKind(Enum):
    String = "String"
    Bytes = "Bytes"
    Number = "Number"


class Value:
    def __init__(
            self,
            kind: ValueKind,
            string: Optional[bytes] = None,
            bytes: Optional[bytes] = None,
            number: Optional[int] = None,
    ):
        self.kind = kind
        self.string = string
        self.bytes = bytes
        self.number = number

    def to_scval(self) -> xdr.SCVal:
        if self.kind == ValueKind.String:
            assert self.string is not None
            return scval.to_enum(self.kind.name, scval.to_string(self.string))
        if self.kind == ValueKind.Bytes:
            assert self.bytes is not None
            return scval.to_enum(self.kind.name, scval.to_bytes(self.bytes))
        if self.kind == ValueKind.Number:
            assert self.number is not None
            return scval.to_enum(self.kind.name, scval.to_int128(self.number))
        raise ValueError(f"Invalid kind: {self.kind}")

    @classmethod
    def from_scval(cls, val: xdr.SCVal):
        elements = scval.from_enum(val)
        kind = ValueKind(elements[0])
        if kind == ValueKind.String:
            assert elements[1] is not None and isinstance(elements[1], xdr.SCVal)
            return cls(kind, string=scval.from_string(elements[1]))
        if kind == ValueKind.Bytes:
            assert elements[1] is not None and isinstance(elements[1], xdr.SCVal)
            return cls(kind, bytes=scval.from_bytes(elements[1]))
        if kind == ValueKind.Number:
            assert elements[1] is not None and isinstance(elements[1], xdr.SCVal)
            return cls(kind, number=scval.from_int128(elements[1]))
        raise ValueError(f"Invalid kind: {kind}")

    def __eq__(self, other: object) -> bool:
        if not isinstance(other, Value):
            return NotImplemented
        if self.kind != other.kind:
            return False
        if self.kind == ValueKind.String:
            return self.string == other.string
        if self.kind == ValueKind.Bytes:
            return self.bytes == other.bytes
        if self.kind == ValueKind.Number:
            return self.number == other.number
        return True

    def __hash__(self) -> int:
        if self.kind == ValueKind.String:
            return hash((self.kind, self.string))
        if self.kind == ValueKind.Bytes:
            return hash((self.kind, self.bytes))
        if self.kind == ValueKind.Number:
            return hash((self.kind, self.number))
        return hash(self.kind)


class Client(ContractClient):
    def set(
            self,
            node: bytes,
            key: str,
            value: Value,
            source: Union[str, MuxedAccount],
            signer: Optional[Keypair] = None,
            base_fee: int = 100,
            transaction_timeout: int = 300,
            submit_timeout: int = 30,
            simulate: bool = True,
            restore: bool = True,
    ) -> AssembledTransaction[None]:
        return self.invoke(
            "set",
            [scval.to_bytes(node), scval.to_symbol(key), value.to_scval()],
            parse_result_xdr_fn=lambda _: None,
            source=source,
            signer=signer,
            base_fee=base_fee,
            transaction_timeout=transaction_timeout,
            submit_timeout=submit_timeout,
            simulate=simulate,
            restore=restore,
        )

    def get(
            self,
            node: bytes,
            key: str,
            source: Union[str, MuxedAccount],
            signer: Optional[Keypair] = None,
            base_fee: int = 100,
            transaction_timeout: int = 300,
            submit_timeout: int = 30,
            simulate: bool = True,
            restore: bool = True,
    ) -> AssembledTransaction[Optional[Value]]:
        return self.invoke(
            "get",
            [scval.to_bytes(node), scval.to_symbol(key)],
            parse_result_xdr_fn=lambda v: (
                Value.from_scval(v)
                if v.type != xdr.SCValType.SCV_VOID
                else scval.from_void(v)
            ),
            source=source,
            signer=signer,
            base_fee=base_fee,
            transaction_timeout=transaction_timeout,
            submit_timeout=submit_timeout,
            simulate=simulate,
            restore=restore,
        )


def parse_domain(domain: str, sub_domain: str = None) -> bytes:
    """
    This function takes a domain and generates the "node" value of the parsed domain.
    This "node" value can be used to fetch data from the contract (either if is a Record or a SubRecord)
    """
    record = keccak.new(digest_bits=256)
    record.update(keccak.new(digest_bits=256).update("xlm".encode()).digest())
    record.update(keccak.new(digest_bits=256).update(domain.encode()).digest())

    if sub_domain:
        sub_record = keccak.new(digest_bits=256)
        sub_record.update(keccak.new(digest_bits=256).update(record.digest()).digest())
        sub_record.update(
            keccak.new(digest_bits=256).update(sub_domain.encode()).digest()
        )
        return sub_record.digest()
    else:
        return record.digest()


if __name__ == "__main__":
    client = Client(
        "CDH2T2CBGFPFNVRWFK4XJIRP6VOWSVTSDCRBCJ2TEIO22GADQP6RG3Y6",
        "https://mainnet.sorobanrpc.com",
        Network.PUBLIC_NETWORK_PASSPHRASE,
    )

    # You need to set the below values
    kp = Keypair.from_secret("S...")
    domain = "scfdemo"  # domain name
    record_type = IPFS_KEY  # IPFS_KEY or IPNS_KEY
    record = "bafybeicqkdg3ljrpeja5gfnz6zq65egxwgpsyv6wfrrltiizo6d7srkhge"  # IPFS CID

    # https://github.com/Creit-Tech/sorobandomains-sc/blob/main/contracts/key-value-db/src/utils.rs#L70
    # subdomain not supported for contract address now.
    subdomain = None

    node = parse_domain(domain, subdomain)
    value = Value(ValueKind.String, string=record.encode("utf-8"))
    try:
        client.set(node, record_type, value, kp.public_key, kp).sign_and_submit()
        print("Success")
    except Exception as e:
        print(f"Error: {e}")

    resp = client.get(node, record_type, kp.public_key).result()
    print(resp.string)