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)